| tags |
|
|---|
Repo-agnostic fix-until-green harness: a worker agent edits the repo, a shell verifier decides “done,” an optional judge can send the worker back — with a fresh context every iteration.
Supports Cursor, Cline (Pass / Credits), OpenCode (Go + BYOK), and Pi workers. The primary judge defaults to Cursor (reviewRuntime unset) but can use any worker runtime via reviewRuntime + reviewModel.
New here? Start with README.intro.md (how the loop works, worker vs judge, why it’s shaped this way). Cost-minmax runtime map: docs/runtime-map.md. Technical deep dive: ARCHITECTURE.md. npm releases: docs/releasing.md.
| Layer | What it does | Blocks completion? |
|---|---|---|
| Worker | Fresh Cursor / Cline / OpenCode / Pi session each iteration; 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 | reviewGateHitl, hitlCheck, hitlOnFailure, Telegram (+ HITL fallback if notify fails) |
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-family judge —
reviewSecondaryRuntimeon Cline SDK (cline-passorcline): 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, AGENT_LOOP_TELEGRAM_*, AGENT_LOOP_CURSOR_TIMEOUT_MS).
Verification checklist authoring: docs/verification-as-skill.md.
Requires Node.js 22+. Install from npm (works in cloud agents and any consumer repo):
# 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
pnpm add -D @earendil-works/pi-coding-agent # Pi BYOKUse CLIs via pnpm exec (or npx) so you do not need a global install:
pnpm exec agent-loop-init
pnpm exec agent-check cursor
pnpm exec agent-loop run .cursor/loops/my-task --runtime cursor --review-gatepnpm add -D @dancingteeth/agent-looper @cursor/sdk
export CURSOR_API_KEY=… # or wrap with your secret manager (Doppler, etc.)
pnpm exec agent-loop-init
# edit .cursor/agent-loop.repo.json
# edit .cursor/loops/my-task/GOAL.md + loop.json + 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 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-gateTarget 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.
.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.
.cursor/loops/my-task/
GOAL.md # frozen spec
loop.json # verify, runtime, optional taskwarriorUuid
verify.sh # measurable shell checks (exit 0 = pass)
VERIFY.skill.md # agent-readable verify procedure (optional; required for verifyMode: skill)
log.ndjson # append-only iteration log (runtime)
run-report.md # human-readable run summary (when exportRunReport)
transcript.ndjson # tool timeline (when exportTranscript)
failure-domains.ndjson # optional — stagnation / max iterations / gate exhaust
failure-context.md # optional — written by meta-loop probe for fix loop
| Field | Default | Purpose |
|---|---|---|
verify |
(required) | Shell command every iteration (usually bash …/verify.sh). Exit 0 = pass. |
verifyMode |
command |
command = shell only. skill = verify agent reads verifySkill, emits VERIFY_RESULT: PASS/FAIL, then runs shell verify on PASS. |
verifySkill |
— | Path to VERIFY.skill.md (required when verifyMode is skill). |
finalVerify |
— | Stricter outer check after inner verify passes. |
Legacy loop.json field syncPostgres maps to syncOnSuccess.
| Field | Default | Purpose |
|---|---|---|
runtime |
cursor |
Worker: cursor | cline-pass | cline | opencode | pi. See docs/runtime-map.md. |
model / escalateModel |
(defaults) | Worker model; escalate on stagnation (OpenCode/Pi: after threshold; Cline: after reasoning ceiling). |
maxIterations |
8 |
Cap implement iterations. |
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) |
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 |
reasoningEffort |
— | Cline: low | medium | high | xhigh | none. Cursor ignores. |
escalateReasoningEffort |
— | Cline reasoning ladder ceiling |
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) |
skills |
— | Explicit …/SKILL.md paths inlined into worker prompts (merged with GOAL refs) |
plugins |
— | Agent Plugins package dirs — discovers skills/*/SKILL.md (docs/agent-plugins.md) |
| 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 |
cursor |
Primary judge runtime (same enum as runtime). Unset → cursor. |
reviewModel |
(resolved) | Judge model for reviewRuntime. Cursor defaults: grok-4.5 when worker is cursor, else composer-2.5. Non-cursor judges use that runtime’s default model. 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) | Cline SDK second-family judge (cline-pass or cline); unset = off |
reviewSecondaryModel |
(default) | Cline model for secondary review |
trustConfig |
false |
Mark this loop's shell commands as pre-reviewed (pairs with --trust-config gate) |
exportRunReport |
true |
Write run-report.md when the loop finishes |
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.
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 |
verifyLogMode: "sidecar" |
reserved — falls back to inline verify output |
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>.
When reviewGate: true and verify passes:
primary review (reviewRuntime + reviewModel; default cursor)
→ optional reviewReproduce path filter
→ optional reviewReproduceAgent KEEP/DROP
→ optional reviewSecondaryRuntime merge (Cline-only)
→ gating blockers remain? → fix iteration (up to maxReviewCycles)
→ else PASS / ADVISORY → complete
Unparseable verdicts retry (unparseableReviewRetries). Gate exhaust can escalate to HITL (reviewGateHitl).
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 - Cursor background wake — on exit, stdout line
AGENT_LOOP_DONE {…}for local Shellnotify_on_output(Cloud Agents: no watcher yet — 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.
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.
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.
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.
| Command | Description |
|---|---|
agent-loop run <dir> |
Single loop |
agent-loop-batch <dir> |
loop-batch.json sequential or meta-loop |
agent-check cursor|cline|opencode |
SDK + API key smoke |
agent-loop-init |
Scaffold templates |
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) |
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)
→ repeat
Post-success (when postQualityReview runs): Cursor quality review → review.md using the repo REVIEWS.md overlay. With reviewGate: true, only gating blockers re-enter the fix loop; completion requires PASS or ADVISORY with no gating blockers. Then: optional Taskwarrior done → hitlCheck (via configured 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 |
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: warn + tip (
--trust-configafter review). --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.
| 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 |
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_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) |
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.
When a local Cursor Agent starts agent-loop in a background shell, the chat can wake on completion without Telegram:
- Background the run with a long
block_until_ms(or0) andnotify_on_outputon pattern^AGENT_LOOP_DONE. - On wake, read the JSON on that stdout line (
complete,exitCode,reason,bundle, optionalrunReport) and/or openrun-report.md. - Opt out:
--no-completion-signal,"completionSignal": falseinloop.json/loop-batch.json, orAGENT_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"}
Human logs stay on stderr; the sentinel is written with fs.writeSync(1, …) so piped stdout (local background shells) 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 local wake.
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.
In-loop review.md / log.ndjson / run-report.md stay gitignored (noisy mid-run). 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.ndjson
Commit 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).
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.
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.
MIT