@dancingteeth/agent-looper
v0.8.3
Published
Fix-until-green harness for coding agents: frozen goal + verify.sh, a fresh worker each round, optional judge review. Works with Cursor, Claude Code, Codex, OpenCode, Cline, Pi, DSH, and Muse.
Downloads
798
Maintainers
Readme
tags:
- documentation
- agents
Agent Looper (@dancingteeth/agent-looper)
Fix-until-green for coding agents. Give it a frozen goal and a verify.sh you already trust. It runs a fresh worker agent each round until the check exits 0, with an optional judge that reviews the result and can send the worker back.
Works with Cursor, Claude Code, Codex, OpenCode, Cline, Pi, DeepSeek Harness (DSH), and Muse, as the worker, the judge, or both. Mix them: a cheap worker with a stronger judge is the usual setup.
If you already loop by hand: the agent says done, CI is still red, you paste the log, you open a new chat. You are the verify step. This package is that loop without you in the middle.
Landing page: looper.dancingteeth.net (static site in site/).
# Core CLI only (init, setup, --version). Add the SDK for the runtime you will run:
pnpm add -D @dancingteeth/agent-looper @cursor/sdk # example: Cursor worker/judge
pnpm exec agent-loop-init
pnpm exec agent-loop-setup # pick worker + judge runtimes (one-screen wizard)
# or draft a loop from an idea: pnpm exec agent-loop-prompt --out .cursor/loops/my-task
# edit GOAL.md
# put the check you keep re-running in verify.sh
pnpm exec agent-loop run .cursor/loops/my-taskEach runtime also needs its own SDK or CLI and key, e.g. @cursor/sdk + CURSOR_API_KEY for Cursor, claude on PATH for Claude Code, opencode for OpenCode. docs/runtime-map.md lists them all; pnpm exec agent-check <runtime> confirms one is wired. Flags and the rest are below. You do not need them for a first green run.
How the loop is shaped: README.intro.md. Technical deep dive: ARCHITECTURE.md (including §1.1 — the harness is a small control-flow graph; the Ralph loop lives inside the worker node). npm releases: docs/releasing.md. What shipped: CHANGELOG.md (current 0.8.3). Embedding this package as a library dependency in another ADE: docs/embed-api.md. Reporting a vulnerability: SECURITY.md.
Supports pluggable agent SDK workers (runtime) and judges (reviewRuntime). Shipped today: Cursor, Cline (Pass / Credits), OpenCode (Go + BYOK), Pi, Codex, DSH (PATH dsh), Muse (PATH muse), Claude (PATH claude, --safe-mode). Defaults and cost notes: docs/runtime-map.md. DSH companion for dsh web (skills + scaffold, not a second harness): docs/dsh-plugin.md. OpenCode companion plugin (1.x + 2.x: skills, detached agent_loop_start, shell guard): plugins/opencode-agent-looper/. Muse Code: docs/muse-runtime.md. Claude Code: docs/claude-runtime.md. To measure cheap-worker claims on a frozen loop: docs/runtime-cost-bench.md. The judge runs on the worker's runtime when reviewRuntime is unset (each runtime has its own judge model, e.g. OpenCode → DeepSeek V4 Pro, Claude → Opus); set reviewRuntime + reviewModel to pair a cheap worker with a different judge.
Features at a glance
| Layer | What it does | Blocks completion? |
| --- | --- | --- |
| Worker | Fresh agent SDK session each iteration (runtime); implements toward GOAL.md | — (does the work) |
| Verifier | Shell verify / finalVerify (exit 0). Optional verifyMode: skill runs a verify agent first (VERIFY_RESULT: PASS/FAIL), then shell. | Yes — hard gate |
| Review | Post-success LLM quality review → review.md (primary judge via reviewRuntime, default cursor). Optional reviewGate re-opens the fix loop on gating findings only. | Only with reviewGate: true |
| Human | HITL checkpoints (hitlProvider), reviewGateHitl / hitlCheck / hitlOnFailure, completion notify (Telegram / webhook / notifyCommand / PR comment) | Closure / alerts |
Review stack (opt-in unless noted):
- Impact-severity — only
severity: error+ recognizedimpacttags gate (data-loss,security-boundary,false-closure,cross-dispatch,verify-bypass). Cosmetic findings stay advisory. - Reproduce-before-report —
reviewReproduce: drop error+impact blockers without a citeable path in the changed-files set. - Fresh reproduce agent —
reviewReproduceAgent: second judge session KEEP/DROP on remaining gating blockers (samereviewRuntimeas primary). - Secondary judge —
reviewSecondaryRuntime(any worker/judge runtime): union gating blockers with primary; skips when primary is PASS/ADVISORY with zero gates.
Factory scale: agent-loop-batch (sequential + meta-loop probe→fix), agent-loop-meta-review (read-only cross-loop report over N bundles).
Ops: stagnation detection, failure-domains.ndjson, Telegram completion reports, secrets via env / your secret manager (CURSOR_API_KEY, CLINE_API_KEY, OPENCODE_API_KEY, OPENROUTER_API_KEY, AI_GATEWAY_API_KEY, AGENT_LOOP_TELEGRAM_*, AGENT_LOOP_CURSOR_TIMEOUT_MS).
Verification checklist authoring: docs/verification-as-skill.md. Freeze a four-part finish line (outcome, scoreboard, permission, budget) plus optional golden artifact — templates/GOAL.template.md. Metric loops: revert if worse than baseline — templates/GOAL.metric.template.md. Visual / taste loops (homepage, mockup, screenshot-as-hero): templates/GOAL.visual.template.md.
Install
Requires Node.js 22+. Install from npm (works in cloud agents and any consumer repo).
CLI telemetry (opt-in, off by default): set AGENT_LOOPER_TELEMETRY=1 to send anonymous usage events (looper_init, looper_run_started, looper_run_finished) to PostHog EU. Requires POSTHOG_PROJECT_API_KEY or AGENT_LOOPER_POSTHOG_KEY. Sends only package version, runtime, OS platform, Node major version, verify pass/fail, duration, and review-gate flag — never repo paths, prompts, or secrets. The marketing site at looper.dancingteeth.net does not use analytics pixels.
# Cursor-only (minimum)
pnpm add -D @dancingteeth/agent-looper @cursor/sdk
# Optional workers
pnpm add -D @cline/sdk # Cline Pass / Credits
pnpm add -D @opencode-ai/sdk opencode-ai # OpenCode Go / BYOK (CLI 1.x)
pnpm add -D @opencode/client # …or with an OpenCode 2.x CLI on PATH
pnpm add -D @earendil-works/pi-coding-agent # Pi BYOK
pnpm add -D @openai/codex-sdk # Codex (ChatGPT / OpenAI)
pnpm add -D @muse-code/sdk # Muse Code (PATH `muse`)Use CLIs via pnpm exec (or npx) so you do not need a global install:
pnpm exec agent-loop-init
pnpm exec agent-loop-setup --out .cursor/loops/my-task # Ink TUI; --plain / --answers for agents
pnpm exec agent-loop-prompt --out .cursor/loops/my-task # idea → GOAL/verify draft → freeze → watch
pnpm exec agent-check cursor
pnpm exec agent-loop run .cursor/loops/my-task --runtime cursor --review-gateQuick start
pnpm add -D @dancingteeth/agent-looper @cursor/sdk
export CURSOR_API_KEY=… # or wrap with your secret manager (Doppler, etc.)
pnpm exec agent-loop-init
# Humans: pnpm exec agent-loop-setup --out .cursor/loops/my-task
# writes repo defaults (runtime, models, review, notify) into
# .cursor/agent-loop.repo.json. Later sparse loop.json files inherit them;
# explicit loop.json keys win. Agents skip the TUI — use --answers or
# copy templates and set verify only.
# edit .cursor/loops/my-task/GOAL.md + verify.sh
pnpm exec agent-check cursor
pnpm exec agent-loop run .cursor/loops/my-task --runtime cursor --review-gateOther workers (after installing the matching optional peer):
# ClinePass / Credits
export CLINE_API_KEY=…
pnpm exec agent-loop run .cursor/loops/my-task --runtime cline-pass
pnpm exec agent-loop run .cursor/loops/my-task --runtime cline
# OpenCode (needs `opencode` on PATH from opencode-ai)
export OPENCODE_API_KEY=… # and/or OPENROUTER_API_KEY / AI_GATEWAY_API_KEY for BYOK
pnpm exec agent-check opencode
pnpm exec agent-loop run .cursor/loops/my-task --runtime opencode
# Pi BYOK
export OPENROUTER_API_KEY=…
pnpm exec agent-check pi
pnpm exec agent-loop run .cursor/loops/my-task --runtime pi
pnpm exec agent-loop run .cursor/loops/my-task --runtime pi --review-runtime pi --review-gate
# Codex (needs `codex` CLI from @openai/codex via the SDK)
export CODEX_API_KEY=… # or OPENAI_API_KEY / ChatGPT login
pnpm exec agent-check codex
pnpm exec agent-loop run .cursor/loops/my-task --runtime codex
# DeepSeek Harness (needs `dsh` on PATH; Node ≥ 22.15)
export DEEPSEEK_API_KEY=… # or DSH credentials-local
pnpm exec agent-check dsh
pnpm exec agent-loop run .cursor/loops/my-task --runtime dsh
# runtime: docs/dsh-runtime.md — dsh web companion: docs/dsh-plugin.md
# Muse Code (needs `muse` CLI + @muse-code/sdk)
export META_API_KEY=… # optional; `muse` login is enough
pnpm exec agent-check muse
pnpm exec agent-loop run .cursor/loops/my-task --runtime muse
# Claude Code (needs `claude` 2.1.169+ on PATH; `claude login`)
# leave ANTHROPIC_API_KEY unset so -p uses subscription quota
pnpm exec agent-check claude
pnpm exec agent-loop run .cursor/loops/my-task --runtime claude
# typical: cheap worker + Claude judge — docs/claude-runtime.mdTarget another checkout:
pnpm exec agent-loop run /path/to/repo/.cursor/loops/fix-foo --repo-root /path/to/repoExample consumer scripts:
{
"scripts": {
"agent:loop": "doppler run -- agent-loop run",
"agent:check": "doppler run -- agent-check cursor",
"agent:init": "agent-loop-init"
}
}Harness maintainers developing this repo itself: build local dist/ with pnpm build and run via pnpm agent:loop (see docs/dogfood.md). Release / trusted publishing: docs/releasing.md.
Repo profile
.cursor/agent-loop.repo.json:
| Field | Purpose |
| --- | --- |
| taskwarriorProject | Taskwarrior project for HITL when hitlProvider is taskwarrior — required for TW HITL |
| hitlProvider | taskwarrior (default), file, github, linear, or command — see docs/hitl-providers.md |
| hitlFileDir | Directory for file provider (default .cursor/hitl) |
| hitlCommand | Shell for command provider |
| hitlLinearTeam | Linear team key or id when hitlProvider is linear |
| syncCommand | Shell after success (or null) |
| notifyCommand | Optional shell on every CLI exit (LOOP_* env) |
| notifyWebhook | Optional JSON POST (url or AGENT_LOOP_NOTIFY_WEBHOOK_URL) |
| notifyPrComment | Comment on the open PR after CLI exit (gh pr comment) |
| defaultBranch | Post-loop diff base (main) |
| agentsFile / reviewsFile | Prompt + review overlay paths |
| loopRiskProfile | Optional keyword merge for postQualityReview: "auto" (see REVIEWS.md ## Loop risk inference) |
| skillsGlob | System prompt skills hint |
| clientName | Cline client label |
| telegramNotify | Optional chat id + onSuccess / onFailure |
Per-loop overrides in loop.json: taskwarriorProject, taskwarriorUuid, hitlCheck, hitlOnFailure, requireNotify, and optional hitlProvider / hitlFileDir / hitlCommand / hitlLinearTeam.
Taskwarrior: use UUID in GOAL.md and loop.json taskwarriorUuid — numeric IDs are recycled. On success with syncOnSuccess, the harness marks that UUID done.
Loop bundle
.cursor/loops/my-task/
GOAL.md # frozen spec (four-part finish line + optional golden)
RESEARCH.md # optional — frozen brownfield map (indexed in the worker prompt)
loop.json # verify, runtime, optional taskwarriorUuid
verify.sh # measurable shell checks (exit 0 = pass; 75 / 127 = env)
setup.sh # optional — harness bootstrap before the first worker
VERIFY.skill.md # agent-readable verify procedure (optional; required for verifyMode: skill)
log.ndjson # append-only iteration log (runtime)
assistant.stream # live token/thinking tail for watch (runtime)
run-report.md # report card + iteration timeline (when exportRunReport)
transcript.ndjson # tool timeline (when exportTranscript)
setup.log # optional — bootstrap evidence (runtime)
verify-logs/ # optional — sidecar verify stdout/stderr (`verifyLogMode`)
failure-domains.ndjson # optional — stagnation / max iterations / gate exhaust
failure-context.md # optional — written by meta-loop probe for fix loop
lessons.md # optional — verify gotchas from prior green runs (harness-owned)Verification (verify / verifyMode)
| Field | Default | Purpose |
| --- | --- | --- |
| verify | (required) | Shell command every iteration (usually bash …/verify.sh). Exit 0 = pass. Exit 75 (EX_TEMPFAIL), exit 127 (command not found), or a VERIFY_CLASS=env line parks as an environment limitation (status: waiting, HITL) — the worker does not iterate. Other non-zero exits are product failures and continue the loop. |
| verifyMode | command | command = shell only. skill = verify agent reads verifySkill, emits VERIFY_RESULT: PASS/FAIL, then runs shell verify on PASS. Skill-verify uses the same iteration agent as the worker (reasoning ladder / escalateModel apply). |
| verifySkill | — | Path to VERIFY.skill.md (required when verifyMode is skill). |
| finalVerify | — | Stricter outer check after inner verify passes. |
| verifyLogMode | sidecar | How verify stdout/stderr reach the next worker. sidecar (default) writes <loop-dir>/verify-logs/ and puts a tail-biased preview + path in the prompt. inline pastes the in-memory capture only (head+tail when long). |
Default is sidecar so failures at the end of vitest/Playwright walls stay visible without bloating every prompt. Set "verifyLogMode": "inline" when verify is always short. In-memory capture is capped (~64KB, head+tail) before sidecar write.
Legacy loop.json field syncPostgres maps to syncOnSuccess.
loop.json — loop control
| Field | Default | Purpose |
| --- | --- | --- |
| runtime | cursor | Worker: cursor | cline-pass | cline | opencode | pi | codex | dsh | muse | claude. Unset when costPreset is set so detection can bind. See docs/runtime-map.md. Same-task cost method: docs/runtime-cost-bench.md. |
| costPreset | — | Named worker+judge stack: minmax (efficiency — cheapest capable worker + strongest included judge; Grok whenever Cursor is installed), balanced (escalate-tier worker, same judge), cursor (Composer + Grok). Detect-bound at parse when runtime/model are unset; explicit keys win. Not Auto. |
| model / escalateModel | (defaults) | Worker model; escalate on identical verifier stagnation or immediately after a hung/timed-out worker (OpenCode/Pi/Codex/DSH/Muse/Claude: after threshold; Cline: after reasoning ceiling — worker fault skips the ceiling). Cursor workers are always Composer 2.5 and have no escalate slug — omit escalateModel (repo defaults that leak a Go escalate are dropped). The judge is reviewRuntime / reviewModel, not escalateModel. |
| maxIterations | 8 | Cap implement iterations. |
| setup | — | Harness bootstrap shell, run once before the first worker. When unset, setup.sh beside GOAL.md is used if that file exists. Failure parks status: waiting (no worker) and restores frozen spec files if setup mutated them. Template: templates/setup.example.sh. |
| maxCostUsd | — | Dollar cap on the budget figure: billed invoice when the runtime reports more than $0 (PAYG), otherwise API list price so a $0 subscription quota still stops. Watch/report show list and billed when they differ (--max-cost). Omit = no cap. |
| stagnationThreshold | 3 | Stop after N identical verifier failures (0 = disable). |
| mode | forward | reverse = clean-room rebuild (templates/GOAL.reverse.template.md) |
| pauseAfterIteration | false | Wait for Enter after each iteration (TTY only) |
| injectFailureContext | false | Read failure-context.md into the prompt (meta-loop fix rounds) |
| lessons | false | Opt-in: read/write lessons.md — bounded verify gotchas from earlier green runs, quoted as data in the stable prompt head |
| deliverables | — | Repo-relative files the worker must create (absolute or .. paths are rejected); harness re-prompts before verify when missing or empty |
| deliverableNudges | 2 | Max extra worker calls per iteration when deliverables are still missing (0–5) |
| syncOnSuccess | true | Run repo profile syncCommand after success |
| notifyTelegram | true | Send completion report when Telegram env + profile are configured |
| telegramAttachReview | true | Attach review.md as a second Telegram message |
| hitlOnFailure | false | Open HITL checkpoint when the loop ends incomplete |
| requireNotify | false | Abort if Telegram preflight fails (also --require-notify) |
| completionSignal | true | Emit AGENT_LOOP_DONE on stdout when the CLI exits (local Cursor wake; Cloud Agents cannot attach a watcher yet) |
| notifyCommand | — | Override repo profile notifyCommand for this loop |
| exportPack | true | Copy curated artifacts to .cursor/loop-exports/<slug>/ (commit-friendly) |
| notifyPrComment | — | Override profile notifyPrComment for this loop |
| preview | — | Optional shell for localhost preview; agent-loop-prompt trust-gates and runs it after a successful grind (agent-loop run does not execute it) |
| reasoningEffort | — | low | medium | high | xhigh | none when the runtime honors it (Cline, Pi, Muse). Omit or none = no extra thinking. Cursor / OpenCode / Codex / DSH / Claude ignore it. |
| escalateReasoningEffort | — | Reasoning ladder ceiling (same runtimes as reasoningEffort). Applies to the worker and skill-verify. |
| reasoningEscalationStep | 1 | Tiers to step per iteration (1 or 2) |
| escalateModelReasoningEffort | — | Reasoning tier on escalated model |
| escalateAfterStagnation | 2 | Identical-failure count before model switch (after reasoning ceiling). Worker timeout / no-tool stall switches immediately and does not wait for this count. |
| stallProbes | false | When true, the first time identical failures hit escalateAfterStagnation, the harness runs two differently guided probe workers (procedure hints vs repo facts), keeps the better workspace, then resumes normal escalation — at most once per run. Workspace snapshots are stored as git objects (worktree + index, no stash); restore puts back the branch and HEAD, rewrites only paths that differ, never follows symlinks, keeps file permissions, never deletes a path the snapshot's ignore rules ignore and never deletes under .cursor/loops/. Limits: ignored files are not snapshotted; restore reverts every change made during the probe round — one loop per worktree, don't edit while probes run. |
| skills | — | Explicit …/SKILL.md paths (merged with GOAL refs). Default prompt is an index (name, description, path) — worker Reads the file when needed. |
| skillDisclosure | index | index = progressive disclosure (0.4.0 default; 0.3.0 always inlined). inline = paste full SKILL.md bodies. Pin the field on any loop that must keep the old in-prompt runbook. |
| plugins | — | Agent Plugins package dirs — discovers skills/*/SKILL.md (docs/agent-plugins.md) |
| research | — | Optional path to a frozen brownfield map. If unset, the harness indexes RESEARCH.md beside GOAL.md when that file exists. Prompt gets path + one-line (worker Reads it); body is not inlined. Template: templates/RESEARCH.example.md. |
loop.json — review & quality
| Field | Default | Purpose |
| --- | --- | --- |
| postQualityReview | auto | Run post-loop review (true / false / auto by inferred risk) |
| reviewRisk | auto | Override inferred risk for postQualityReview: "auto" (high / medium / low) |
| loopRiskProfile | — | Per-loop keyword merge for risk inference (high / medium / low arrays) |
| reviewGate | false | When true, gating blockers re-enter the fix loop (up to maxReviewCycles) |
| reviewRuntime | worker runtime | Primary judge runtime (same enum as runtime). Unset → the worker's runtime with its default judge model. |
| reviewModel | (resolved) | Judge model for reviewRuntime. Cursor defaults: grok-4.7 when worker is cursor, else composer-2.5 (Grok 4.6 and 4.5 still allowed). OpenCode judge (reviewRuntime: "opencode") defaults to opencode-go/deepseek-v4-pro (worker stays Flash). DSH judge (reviewRuntime: "dsh") defaults to deepseek-official/deepseek-v4-pro. Codex judge (reviewRuntime: "codex") defaults to gpt-5.6-sol (worker stays Luna; optional gpt-6-astra). Muse judge (reviewRuntime: "muse") defaults to muse-spark-1.3 (worker stays contributor). Claude judge (reviewRuntime: "claude") defaults to opus (Claude Code’s latest Opus, currently Opus 5; worker stays Sonnet). Pi / Cline judges use that runtime’s worker default. Never Composer Fast on cursor. |
| maxReviewCycles | 2 | Review-triggered fix rounds when reviewGate is on |
| reviewGateHitl | false | On gate exhaust, open a HITL checkpoint (hitlProvider) instead of hard-fail only |
| unparseableReviewRetries | 2 | Retries when verdict cannot be parsed |
| reviewBlockerRecheck | true | On BLOCKERS fix rounds, lighter scope-limited re-check |
| reviewReproduce | false | Path filter on error+impact blockers (changed-files set) |
| reviewReproduceAgent | false | Fresh KEEP/DROP session on gating blockers (needs reviewReproduce; uses primary reviewRuntime) |
| reviewSecondaryRuntime | (unset) | Second residual judge (cursor | cline-pass | cline | opencode | pi | codex | dsh | muse | claude); unset = off |
| reviewSecondaryModel | (default) | Model for secondary review (defaults per that runtime) |
| trustConfig | false | Mark this loop's shell commands as pre-reviewed (pairs with --trust-config gate). Only consulted in strict mode (--require-trust-config); by default untrusted commands warn and still run. |
| exportRunReport | true | Write run-report.md when the loop finishes (report card + timeline) |
| exportTranscript | true | Record tool events in transcript.ndjson and per-iteration tool counts in log.ndjson |
Blocker grammar: ship REVIEWS.md from templates/REVIEWS.md. Library: reviewVerdictAllowsCompletion takes a full ParsedReview for impact-severity gating.
loop.json — reserved fields (experimental)
These fields validate in loop.json but their pipeline hooks are not executed yet.
The harness logs a loop extension preflight note on every run (CLI, batch, and direct
library calls) when they are configured — do not rely on them gating anything:
| Field | Status |
| --- | --- |
| smokeScripts | reserved — post-verifier hook not implemented |
| siblingRepos | partially wired — recorded in log.ndjson; cross-repo verify not implemented |
| verifyPreflight | reserved — not implemented |
postQualityReview: "auto" and loop risk
When postQualityReview is "auto" (default), the harness infers high / medium / low
from keywords in GOAL.md + the verify command. Review runs when tier is not low.
reviewGate: true always runs review regardless.
Merge order (each layer adds keywords; first match wins high → medium → low):
- Harness defaults (
DEFAULT_LOOP_RISK_KEYWORDSinloopRiskProfile.ts) REVIEWS.md→## Loop risk inference→### HIGH/MEDIUM/LOW.cursor/agent-loop.repo.json→loopRiskProfileloop.json→loopRiskProfile(per-loop merge)
Override inference entirely: "reviewRisk": "high" | "medium" | "low" in loop.json.
Preview without running the loop:
agent-loop-review-preview .cursor/loops/my-taskExample repo overlay (agent-loop.repo.json; full sample: templates/agent-loop.repo.json.example):
{
"loopRiskProfile": {
"high": ["stripe-webhook", "crm-admin"],
"medium": ["checkout"],
"low": ["copy-only"]
}
}Example per-loop override (loop.json):
{
"postQualityReview": "auto",
"reviewRisk": "auto",
"loopRiskProfile": { "high": ["payment-refund"] }
}CLI overrides: --mode reverse, --pause-after-iteration, --review-gate, --no-telegram, --review-runtime <id>, --review-model <id>, --review-secondary-runtime <id>, --review-secondary-model <id>.
Review gate flow
When reviewGate: true and verify passes:
primary review (reviewRuntime + reviewModel)
→ optional reviewReproduce path filter
→ optional reviewReproduceAgent KEEP/DROP
→ optional reviewSecondaryRuntime merge (verdict merge, not error fallback)
→ gating blockers remain? → fix iteration (up to maxReviewCycles)
→ else PASS / ADVISORY → completeIf the primary judge session errors (timeout, provider fault, text-with-no-tool-calls), the harness retries once, then runs reviewSecondary as a fallback judge (without a second merge pass on the same model). When both fail, the run parks (status: waiting, CLI exit 2, failure-domains.ndjson reason review_judge_failed) — verify already passed.
Unparseable verdicts retry (unparseableReviewRetries). Gate exhaust can escalate to HITL (reviewGateHitl). Gating bullets (severity: error + a recognized impact tag) keep the gate open even if a later ### Verdict — PASS heading appears in the same review.md. Heading vs body token disagreement with no gating bullets is unparseable.
run-report.md includes billing token rows (uncached in / out / cache read / cache write by worker vs judge) and an expected tool errors table when any session recorded tool_end failures or plain-text tool stalls.
Ralph loop alignment
Implements the Ralph loop pattern:
- Monolithic — one repo, one process, one task per loop
- Fresh context each iteration; progress in files and git
- Shell backpressure (
verify/finalVerify) as the deterministic done signal - Verification-as-skill —
verify.sh+VERIFY.skill.md; optionalverifyMode: skill - Watch the loop —
log.ndjson,run-report.md,transcript.ndjson, stagnation, optional--pause-after-iteration - Completion sentinel — stdout
AGENT_LOOP_DONE {…}when the CLI exits (for attached local Shellnotify_on_outputor log grep; do not background the job — see below) - Failure domains —
failure-domains.ndjsonon stagnation, max iterations, or review-gate exhaustion - Meta-loop — probe →
failure-context.md→ fix → re-probe
Forward = incremental fix-until-green. Reverse = clean-room prompt guidance; enforce scope via verify and GOAL.md.
Meta-loop (probe → fix → re-probe)
In loop-batch.json:
{
"metaLoop": {
"probe": "system-smoke",
"fix": "fix-from-smoke",
"maxCycles": 3
},
"hitlCheck": "Manual QA after meta-loop",
"taskwarriorProject": "my-project"
}Cycle: probe → on failure write failure-context.md into the fix bundle → fix with injectFailureContext → re-probe. Stops when probe passes or maxCycles is exhausted. See templates/loop-batch.meta.example.json.
Sequential batch with per-item rubrics
Each loops[] entry is either a sibling loop name/path (string) or { "path": "...", "rubric": "..." }. When rubric is set, the batch runner injects a volatile Batch rubric section into that loop’s worker prompt; shell verify remains the exit gate. See templates/loop-batch.example.json.
Cross-loop meta-review
Read-only aggregator over N loop bundles (does not re-run workers or flip per-loop complete flags):
agent-loop-meta-review .cursor/loops --out-dir /tmp/meta-out
agent-loop-meta-review .cursor/loops --review-runtime opencode --review-model openrouter/anthropic/claude-sonnet-4
agent-loop-meta-review .cursor/loops/a .cursor/loops/b --hitl --project my-projectCollects latest review.md*, log.ndjson, failure-domains.ndjson, and diff stat vs defaultBranch. When in-loop files are missing (typical after a cloud clone — those paths are gitignored), falls back to .cursor/loop-exports/<slug>/. Prompt brief: docs/meta-review-prompt.md.
CLIs
| Command | Description |
| --- | --- |
| agent-loop run <dir> | Single loop |
| agent-loop watch <dir> | Live progress: Ink watch view (TTY) or structured phase lines; --snapshot / --pulse print one frame and exit. Ink s refreshes pid/log/stream health; assistant tokens and Cursor thinking/tools tail in the pane. |
| agent-loop-batch <dir> | loop-batch.json sequential or meta-loop |
| agent-check cursor\|cline\|opencode\|pi\|codex\|dsh\|muse\|claude | SDK + API key smoke (dsh: PATH CLI + Node ≥ 22.15; muse: PATH muse + @muse-code/sdk; claude: PATH CLI 2.1.169+ + --safe-mode, no quota spend) |
| agent-loop-init | Scaffold templates + check-running-loops skill (.cursor/skills and .agents/skills) |
| agent-loop-setup | Ink TUI / --plain / --answers wizard: repo defaults in .cursor/agent-loop.repo.json plus loop.json for --out |
| agent-loop-prompt | Ink TUI: type an idea → judge scaffolds GOAL/verify → freeze lint + confirm → agent-loop run watch; optional preview after success. Failed runs print a Doppler-wrapped resume when DOPPLER_PROJECT / DOPPLER_CONFIG are set. |
| agent-loop-doctor | Validate install / dist/ integrity; model pricing drift vs CLINE_PASS_LOOP_MODELS |
| agent-loop-meta-review | Cross-loop meta-review (read-only) |
| agent-loop-review-run | Post-loop quality review for one bundle |
| agent-loop-review-preview | Preview review risk / prompt |
| agent-loop-export-run | Regenerate run-report.md from log.ndjson (+ optional transcript.ndjson) |
Architecture
GOAL.md + loop.json
→ fresh worker agent
→ verify (command or skill + command)
→ optional review gate
→ log.ndjson
→ run-report.md (+ transcript.ndjson when enabled)
→ .cursor/loop-exports/<slug>/ (curated pack; commit-friendly)
→ repeatPost-success (when postQualityReview runs): quality review → review.md using the repo REVIEWS.md overlay. With reviewGate: true, only gating blockers re-enter the fix loop; a quoted ### Verdict — PASS cannot close the gate while those bullets remain. Completion requires PASS or ADVISORY with no gating blockers. Then: optional linked-task completion (e.g. Taskwarrior done) → hitlCheck (via hitlProvider) → syncCommand.
Stderr prints token totals and estimated USD (ClinePass may include cached-input counts).
| Layer | Role | Blocks loop? |
| --- | --- | --- |
| Shell verify / finalVerify (+ optional skill pre-pass) | Deterministic judge | Yes |
| postQualityReview (no gate) | Advisory LLM | No |
| reviewGate: true | Gate on gating blockers / unparseable verdict | Yes |
Threat model
For trusted checkouts you control:
verify/finalVerify/syncCommandrun viashell: true— malicious config = arbitrary shell.- Verifier stdout/stderr is injected into the next worker prompt (soft guardrails only).
- On start, the CLI prints configured shell commands and flags obvious exfil patterns (
curl,wget,| sh, backticks,$()).
Trust gate (opt-in strict mode):
- Default is permissive: the run proceeds after printing the warning + tip (
--trust-configafter review). Nothing is blocked unless you enable strict mode below — do that before running loop bundles you did not author. --require-trust-configorAGENT_LOOP_REQUIRE_TRUST_CONFIG=1: abort unless you pass--trust-config, settrustConfig: trueinloop.json, orAGENT_LOOP_TRUST_CONFIG=1.- Dogfood / CI: set
trustConfig: trueon known-safe loop bundles, or exportAGENT_LOOP_TRUST_CONFIG=1in Doppler.
Only run on repos and loop bundles you trust. Review loop.json and .cursor/agent-loop.repo.json first.
Environment variables
| Variable | Role |
| --- | --- |
| CURSOR_API_KEY | Cursor SDK auth (worker and/or default judge) |
| CLINE_API_KEY | Cline SDK auth (optional peer runtime / secondary judge) |
| OPENCODE_API_KEY | OpenCode Go auth (optional peer; https://opencode.ai/go) |
| OPENROUTER_API_KEY | OpenRouter BYOK for OpenCode / Pi workers and judges (including :free slugs) |
| AI_GATEWAY_API_KEY | Vercel AI Gateway BYOK for OpenCode (vercel/… models; harness auth.set) |
| CODEX_API_KEY / OPENAI_API_KEY | Codex SDK auth (optional; else ChatGPT CLI login) |
| META_API_KEY | Muse Code Model API (optional; else muse CLI login) |
| AGENT_LOOP_VERBOSE | 1 / true — extra stderr stream detail |
| AGENT_LOOP_CURSOR_TIMEOUT_MS | Cursor run timeout in milliseconds (default 2700000 = 45m). Must be a positive number; validated before Agent.create so a bad value fails without burning a paid run. On timeout the harness cancels the remote run. |
| AGENT_LOOP_MUSE_TIMEOUT_MS | Muse run timeout in milliseconds (default 2700000 = 45m). On timeout the harness closes muse serve immediately. |
| AGENT_LOOP_TRUST_CONFIG | 1 — treat shell config as reviewed/trusted |
| AGENT_LOOP_REQUIRE_TRUST_CONFIG | 1 — abort unless trust is set (CLI / env / loop.json) |
| AGENT_LOOP_TELEGRAM_BOT_TOKEN | Telegram bot token (fallback: TELEGRAM_BOT_TOKEN) |
| AGENT_LOOP_TELEGRAM_CHAT_ID | Telegram chat id (or telegramNotify.chatId in the repo profile) |
| AGENT_LOOP_NO_COMPLETION_SIGNAL | 1 — skip AGENT_LOOP_DONE stdout line on CLI exit |
| AGENT_LOOP_NOTIFY_WEBHOOK_URL | JSON webhook URL when notifyWebhook is enabled without inline url |
| AGENT_LOOP_PR_NUMBER | PR number for notifyPrComment (fallback: GH_PR_NUMBER, then gh pr view) |
Telegram completion reports
On finish (success or failure), optional short report:
- Status, repo, bundle/batch, iterations, reason
- Token/cost line
- Review verdict when present
- Last verifier snippet on failure
Optional second message: attach review.md as a document. Opt out with "telegramAttachReview": false or profile "attachReview": false.
Setup:
- Bot token in env:
AGENT_LOOP_TELEGRAM_BOT_TOKEN(orTELEGRAM_BOT_TOKEN) - Chat id:
AGENT_LOOP_TELEGRAM_CHAT_IDortelegramNotify.chatIdin the repo profile - Inject secrets the same way you already run the loop (
doppler run, direnv, CI secrets, …)
Opt out: "notifyTelegram": false or --no-telegram. Notify send failures are non-blocking for exit code, but if Telegram was configured and a failure report did not land, the harness opens a HITL checkpoint (notify_failed) via hitlProvider. Use --require-notify / requireNotify: true to abort before the loop when getMe preflight fails.
Running from Cursor chat
Cursor’s agent Shell is not a terminal. block_until_ms: 0 (or the IDE background button) is a child the IDE reaps at ~5 minutes (status: aborted, exit_code: unknown, often pnpm **255`) while the worker is still mid-turn. That is not the harness: TTFB stall is 3 min with no events; OpenCode no-tool stall is 8 min of text without tools; overall timeout is 45 min.
If you are the agent in this chat, you start the loop. Do not print a command and tell the human to run it. Walk-away is not your fallback.
| Intent | How |
| --- | --- |
| This chat (default) | Shell attached: block_until_ms ≥ 45m (2700000). notify_on_output on ^AGENT_LOOP_DONE . |
| Human asked to walk away | Their terminal (pnpm agent:loop …). Telegram / HITL / webhook wake them. |
Never block_until_ms: 0 for agent-loop or agent-loop-batch. Re-attach after an abort; do not treat it as a harness failure.
AGENT_LOOP_DONE is a stdout sentinel for an attached watcher or log grep. Opt out: --no-completion-signal, "completionSignal": false, or AGENT_LOOP_NO_COMPLETION_SIGNAL=1.
Example payload:
AGENT_LOOP_DONE {"v":1,"kind":"loop","bundle":".cursor/loops/my-task","complete":true,"exitCode":0,"reason":"Verifier passed (exit 0).","iterations":2,"runReport":".cursor/loops/my-task/run-report.md"}exitCode in the JSON matches the CLI process exit code:
| Code | Meaning |
| --- | --- |
| 0 | Success (complete: true) |
| 1 | Fatal harness error (uncaught throw, bad argv) |
| 2 | Incomplete or parked — verify/review gate/stagnation (status: continue) or human/provider park (status: waiting; budget, env/setup, judge unavailable, transient worker cap; complete: false) |
Human logs stay on stderr; the sentinel is written with fs.writeSync(1, …) so piped stdout is not lost before process.exit. Side channels (notifyWebhook / notifyCommand / PR comment) run after the sentinel and are time-capped so a hung hook cannot delay wake.
Cloud Agents
Cloud Agent Shell does not expose notify_on_output today, so this chat cannot attach a regex watcher even though the harness still emits AGENT_LOOP_DONE. Until Cursor adds that (or an equivalent wake), treat cloud completion as:
notifyWebhook— JSON POST to Slack/Discord/n8n (AGENT_LOOP_NOTIFY_WEBHOOK_URLin Doppler)notifyPrComment: true—gh pr commenton the branch PR (set when the cloud agent already opened a PR)- Telegram (
notifyTelegram+ env;--require-notifyto fail closed) - HITL (
hitlOnFailure, Linear / file / github with an issue-capable token) - Export packs — commit or attach
.cursor/loop-exports/<slug>/so meta-review and humans can read reviews without gitignored mid-run files
Do not promise in-chat wake from AGENT_LOOP_DONE on Cloud Agents.
Export packs (cloud / PR audit)
In-loop review.md / log.ndjson / run-report.md / verify-logs/ stay gitignored (noisy mid-run). agent-loop-init adds a block for loop artifacts including .cursor/loops/**/verify-logs/. When exportPack: true (default), each finished loop also writes a curated snapshot to:
.cursor/loop-exports/<slug>/
SUMMARY.md
meta.json
run-report.md # when present
review.md # latest review when present
log-tail.ndjson # last ~40 log lines
failure-domains.ndjsonCommit that directory (or attach it on the PR) so cloud clones and meta-review are not a black box. Cloud agent tip: after the loop, git add .cursor/loop-exports && git commit && git push on the loop branch. Batch completion webhooks/PR comments list every existing child export pack (comma-separated / bullet list). Packs are skipped when the loop dir is outside the repo (vitest tmpdirs) so .cursor/loop-exports does not fill with ..__var__folders__… slugs.
notifyWebhook + PR comments
Repo profile:
{
"notifyWebhook": { "onSuccess": true, "onFailure": true },
"notifyPrComment": true
}Put the URL in Doppler as AGENT_LOOP_NOTIFY_WEBHOOK_URL (or set notifyWebhook.url). Payload is JSON (v, kind, bundle, complete, exitCode, reason, exportPack, …). POSTs are aborted after ~8s; stderr logs redact query/hash from the URL.
notifyPrComment runs gh pr comment for the current branch’s PR (or AGENT_LOOP_PR_NUMBER). Same gh auth as GitHub HITL — works with a user/PAT that can comment; GitHub App tokens often can comment on PRs even when they cannot issue create.
notifyCommand (shell fallback)
Optional shell with LOOP_* env (LOOP_EXPORT_PACK, LOOP_RUN_REPORT, …) when you need custom logic beyond JSON webhook. Non-blocking; ~15s timeout; shell-trust gated. Opt out: --no-notify-command.
License
MIT
