Three-agent autonomous iteration loop for a git workspace. You converse with the Planner to design a plan; on your approval, the Planner spawns the Coder + Reviewer subloop, monitors it, and pushes you (wechat → whatsapp → email fallback chain) only when something needs your attention.
This page is the operator reference.
- Goal is exploratory and you want to design the plan with the agent
before running. The Planner will read your workspace, surface ambiguity,
and write
plan.md/goal.jsonwith you. - You want a long-running loop you can walk away from. The Planner pushes you on regressions, completion, decisions, or stalls; otherwise it stays silent.
- You can write down what "better" means as a shell command (test pass count, latency, loss, gate completion). Without that anchor, Reviewer has nothing to ratchet on.
| Agent | Default | cwd | Owns |
|---|---|---|---|
| Planner | claude / opus | workspace | strategy, plan.md, goal.json, talking to you |
| Coder | claude / sonnet | workspace | code changes, eval execution |
| Reviewer | claude / sonnet | <workspace>/tasks/<run_id>/reviewer_sandbox/ |
distrust audit; advance / hold / rollback |
Each role can use any built-in engine, or a custom engine config supplied by a
local caller (custom engines name an executable, so the HTTP API does not accept
them — see tools.md). If a non-Claude role omits model, that CLI
uses its own default model rather than receiving the Claude opus / sonnet
defaults. Role instructions are included in-band for engines that do not expose a
native system-prompt flag.
Engines without native multi-turn conversation (Cursor, OpenCode, one-shot custom
engines) spawn a fresh process per send, so the dispatcher replays that role's
transcript in-band as a <conversation_history> block, oldest turns dropped past a
character budget. Claude, Codex and Antigravity keep context themselves and get no
replay.
The Planner runs read-only so strategy cannot turn into source edits, and that is
enforced by the engine rather than requested politely: Claude uses plan mode,
Antigravity and Cursor use their plan modes, and OpenCode gets a generated
clawo-readonly agent that denies edit/bash/external_directory (its built-in
plan agent is a user-overridable preset that denies neither, so a "read-only"
session could otherwise still author files through a shell heredoc). A custom
Planner receives permissionMode: 'manual' and its CustomEngineConfig must
map that mode to the CLI's read-only flag — if it cannot, the session refuses to
start rather than silently running write-enabled.
Coder and Reviewer engine/model choices can be overridden by the first successful
spawn_subagents; later attempts to change an already-started role are rejected
instead of silently diverging from the running session.
Coder and Reviewer never speak to you directly. Anything they observe flows through the Planner. The Planner decides what to surface and what to absorb.
1. autoloop_start { run_id, workspace } → Planner session ready
2. autoloop_chat { run_id, "<your goal>" } → Planner reads workspace,
drafts plan.md + goal.json,
asks "ready to spawn?"
3. autoloop_chat { run_id, "go" } → Planner emits spawn_subagents
4. Coder + Reviewer self-iterate → ledger writes per iter
5. Planner pushes you on target_hit / regression / decision / stall
6. Run terminates on target hit, plan-defined max_iters, or your terminate.
# Start a run (creates Planner session)
curl -X POST http://127.0.0.1:18789/v1/openclaw/tools/autoloop_start \
-H 'content-type: application/json' \
-d '{"run_id":"my-run","workspace":"/abs/path/to/workspace"}'
# Chat with the Planner
curl -X POST http://127.0.0.1:18789/v1/openclaw/tools/autoloop_chat \
-H 'content-type: application/json' \
-d '{"run_id":"my-run","text":"Read the workspace and design a plan to fix X"}'
# Inspect state
curl http://127.0.0.1:18789/autoloop/my-run/state
# Live SSE stream (the 3-pane UI subscribes here)
curl http://127.0.0.1:18789/autoloop/my-run/events
# Reset Coder if it drifts (lazy; eager_restart=true to start a fresh session immediately)
curl -X POST http://127.0.0.1:18789/v1/openclaw/tools/autoloop_reset_agent \
-H 'content-type: application/json' \
-d '{"run_id":"my-run","agent":"coder","eager_restart":true}'
# Stop
curl -X POST http://127.0.0.1:18789/v1/openclaw/tools/autoloop_stop \
-H 'content-type: application/json' \
-d '{"run_id":"my-run","reason":"done"}'| Tool | Args | What |
|---|---|---|
autoloop_start |
run_id, workspace, per-role *_engine?, *_model?, *_custom_engine?, send_timeout_ms? |
Start a run; launches Planner and stores Coder/Reviewer defaults. Each custom role requires its matching config. |
autoloop_chat |
run_id, text |
Send a chat message to the Planner; returns the Planner's reply. |
autoloop_status |
run_id |
Current state (status, iter, push count, subagents_spawned). |
autoloop_list |
— | All active runs in this manager process. |
autoloop_stop |
run_id, reason? |
Terminate; stops Planner / Coder / Reviewer. |
autoloop_reset_agent |
run_id, agent ('planner' / 'coder' / 'reviewer'), force?, eager_restart? |
Reset one subagent. Planner reset requires force: true. |
The Planner controls the run by emitting fenced ```autoloop JSON blocks
inside its replies. The dispatcher parses them out and applies them. You
never see the JSON — only the Planner's narrative.
| Tool | Args | What |
|---|---|---|
notify_user |
level ('info' / 'warn' / 'decision' / 'error'), summary, detail?, channel? ('auto' / 'wechat' / 'webchat' / 'both' / 'email') |
Push you out-of-band. |
spawn_subagents |
coder_engine?, coder_model?, reviewer_engine?, reviewer_model?, initial_directive? |
Start Coder + Reviewer. Omitted values inherit run defaults. An engine change without a model uses the new engine's default. Once a role session has started, changing its engine/model is rejected. Custom configs cannot be emitted by Planner. Only after explicit user approval. |
send_directive |
goal, constraints?, success_criteria?, max_attempts? |
Next iter's instruction to Coder. |
pause_loop |
reason |
Halt subloop at next iter boundary; chat keeps working. |
resume_loop |
— | Resume after pause. |
terminate |
reason |
End run. |
update_push_policy |
partial PushPolicy | Mutate notification rules (e.g. when you say "tell me every iter"). |
write_plan |
content (full plan.md body), commit_message? |
Write plan.md to the workspace and git-commit. The only way the Planner can author plan.md — Write/Edit are stripped from the Planner session as a hard role boundary. Re-running replaces the whole file. |
write_goal |
content (full goal.json body), commit_message? |
Same, for goal.json. Content is JSON-validated before write; malformed content errors back to the Planner. |
Custom engine configs are accepted only by autoloop_start (or the HTTP resume
body), never through Planner output. This keeps config fields such as env and
static CLI arguments out of the Planner transcript and decisions.jsonl.
The central registry persists only each role's engine and model, including the
effective Coder/Reviewer selection after a successful spawn. Resume leaves the
prior append-only row untouched until startup succeeds, so a transient CLI
failure cannot erase the run. When resuming a run that uses custom, provide
the matching planner_custom_engine, coder_custom_engine, or
reviewer_custom_engine again; otherwise resume fails with a clear
configuration error rather than silently switching to Claude. Custom config
shape is validated at runtime, while its env and static CLI arguments remain
out of registry and audit records. See multi-engine.md
for the CustomEngineConfig shape.
| Event | Default |
|---|---|
| on_start | info / wechat ("loop started, will notify on issues") |
| on_iter_done_ok | silent |
| on_target_hit | info / both (webchat + wechat) |
| on_metric_regression_2 | warn / both |
| on_reviewer_reject_2 | warn / both |
| on_phase_error | error / both |
| on_stall_30min | warn / wechat |
| on_decision_needed | decision / both |
5-minute dedup on (level, summary) prevents duplicate pushes from the same
event. Channel chain: auto walks wechat → whatsapp → email; wechat /
webchat / email route directly; both does webchat (if session known)
- wechat fallback chain.
on_phase_errorandon_decision_neededcannot be set tosilent: trueby Planner —update_push_policystrips the flag and records the attempt indecisions.jsonl(these channels are the operator's lifeline; they stay loud).
Each agent's context is monitored after every turn. When getStats().contextPercent
crosses the per-agent threshold the dispatcher invokes /compact with a
role-tuned hint (compactSummaryFor). Defaults: Planner 80 %, Coder 70 %,
Reviewer 70 %. Override per run via compactThresholds. A 30 s debounce
prevents re-fire while post-compact stats settle. Events: compact is
emitted on the dispatcher EventEmitter AND appended to decisions.jsonl.
Subprocess deaths (Claude session lost), failed git commit in an iter, and
other phase-bound failures surface as phase_error messages instead of
silently masquerading as a "clarification request". The runner counts
consecutive phase_errors and:
- Fires
on_phase_erroron each one (defaults to error / both channels). - After
phaseErrorCircuitconsecutive errors (default 3) emits adecision-level push and an automaticterminate { reason: 'phase_error_circuit' }.
A successful (non-error) iter_done resets the counter. Override the
threshold via AutoloopConfig.phaseErrorCircuit.
reviewer_memory.md is read at Reviewer-session start and injected as a
frozen <frozen_memory_snapshot> block into the system prompt. It stays
constant for the lifetime of that session so Claude's prefix cache hits.
Reviewer can append fresh observations to the file on disk; those edits
become visible only on the next Reviewer reset (autoloop_reset_agent
with agent: 'reviewer', eager_restart: true).
<ledger>/decisions.jsonl is the auditable trail of runner / dispatcher
decisions:
| Kind | When |
|---|---|
spawn_subagents |
Planner emits spawn_subagents |
reset_agent |
Any agent reset (manual or auto-recovery) |
compact |
Auto-compact fires |
update_push_policy |
Planner mutates the policy |
policy_silence_blocked |
Planner tried to silence a critical channel |
phase_error |
Surfaced from dispatcher to runner |
terminate |
Run ends (planner reason or phase_error_circuit) |
JSONL, one entry per line, ts-prefixed.
<workspace>/tasks/<run_id>/
├── plan.md # Planner-authored, git-committed
├── goal.json # Planner-authored, git-committed
├── push_log.jsonl # every notify_user attempt + channel used
├── decisions.jsonl # runner / dispatcher audit trail (see above)
├── reviewer_sandbox/ # Reviewer cwd; restaged per iter
│ ├── plan.md # copy
│ ├── goal.json # copy
│ ├── iter-N/ # this iter's directive + diff + eval
│ ├── prior_verdict.json
│ ├── reviewer_memory.md # persistent (frozen-injected at session start)
│ └── reviewer_log.jsonl # persistent (Reviewer's append-only audit log)
└── iter/<n>/
├── directive.json # Planner → Coder (schema_version: 1)
├── eval_output.json # what Coder reported (schema_version: 1)
├── diff.patch # git diff of the iter
├── verdict.json # Reviewer decision + audit notes (schema_version: 1)
└── coder_summary.txt
The orchestrator git-commits each iter automatically. Coder must NOT call
git commit itself — that confuses the diff log. If git commit fails
inside an iter (pre-commit hook reject, signing key missing, …) the
dispatcher emits a phase_error instead of writing iter_artifacts, so
the failure is visible to the runner and counts toward the circuit.
Every JSON artifact in the ledger carries a schema_version field (currently
1) to make future migrations explicit.
| Endpoint | Returns |
|---|---|
GET /autoloop/list |
{ ok, runs: AutoloopState[] } |
POST /autoloop/new |
{ ok, run_id, planner_session } — body { workspace, run_id?, planner_engine?, planner_model?, planner_custom_engine?, coder_engine?, coder_model?, coder_custom_engine?, reviewer_engine?, reviewer_model?, reviewer_custom_engine?, send_timeout_ms? } |
GET /autoloop/<id>/state |
{ ok, state: AutoloopState } — also returns a terminated-state stub reconstructed from the registry for runs that aren't in this process's memory, so the dashboard can open historical runs without 404'ing. |
GET /autoloop/<id>/push_log |
{ ok, entries: PushLogEntry[] } — served from the ledger via autoloopStatus, so historical runs work the same as live ones. |
GET /autoloop/<id>/chat_history |
{ ok, entries: ChatEntry[] } — replays <ledger>/chat.jsonl. The dashboard fetches this when opening a run so the Planner-pane conversation survives a page refresh / cross-process / re-opening a terminated run. Returns [] when the file doesn't exist (e.g. runs that predate the chat-history feature). |
GET /autoloop/<id>/events |
SSE: snapshot / message / state / push / iter_done / planner_reply / planner_error / coder_reply / reviewer_reply / terminated. For runs that are NOT in this process's memory (terminated, or live in another process), the endpoint emits a single-shot snapshot + terminated then closes — the dashboard's existing handlers render history without hanging. |
POST /autoloop/<id>/chat |
202 { ok, queued: true } — body { text }. Fire-and-forget: the Planner's reply streams back via the /events SSE channel as a planner_reply event (or planner_error on failure); the HTTP response intentionally does NOT wait for it, because first-contact replies routinely exceed reverse-proxy idle limits (e.g. Cloudflare Tunnel cuts at ~100s → 524). 400 on empty text, 404 when the run is not in this process's memory. The MCP autoloop_chat tool path keeps the synchronous await-and-return-reply semantics (it runs in-process). |
POST /autoloop/<id>/resume |
{ ok, state } — restore the role engine/model choices from the registry and re-create dispatcher + runner. Optional body fields planner_custom_engine, coder_custom_engine, reviewer_custom_engine must be supplied again for roles using custom because configs are intentionally not persisted. Existing engine-specific conversation resume behavior is reused where supported; chat.jsonl remains the visual history fallback. 404 when the registry has no record. |
POST /autoloop/<id>/delete |
{ ok } — stops the runner if still alive, scrubs the row from ~/.claw-orchestrator/autoloop-registry.jsonl, and purges persistedSessions so the run cannot be /resume'd back. The ledger directory under <workspace>/tasks/<run_id>/ is kept on disk. 404 if the run was not present in either memory or the registry. |
The 3-pane UI consumes these endpoints:
- Left: Planner chat (subscribes to
planner_reply) - Center: Coder activity (
coder_reply+iter_done) - Right: Reviewer verdicts (
reviewer_reply) - Top bar: state (status / iter / metric)
- Bottom: push_log
The UI itself ships in a separate cross-repo PR.
The Planner authors goal.json based on your conversation. There is no hard schema — the Coder reads what's there and runs the eval the Planner wrote down. A typical shape:
The Planner will riff on this shape during your chat and ask if it's right.
- ❌ Coder does NOT modify
plan.md,goal.json, or anything undertasks/. Planner owns those. - ❌ Coder does NOT manually
git commit— orchestrator commits per iter. - ❌ Reviewer modifies nothing outside its sandbox cwd.
- ❌ Reviewer never pings Planner / Coder for clarification — operates from artifacts only.
- ✅ Coder leaves notes in
coder_notes.mdfor things future iters need to know. - ✅ Reviewer accumulates "fakery patterns I've seen" in
reviewer_memory.md(persists across iters). - ✅ Reviewer defaults to
holdunder uncertainty; onlyadvanceafter independent verification.
scripts/smoke-autoloop.ts runs a buggy add_two scenario end-to-end with
Opus Planner + Sonnet × 2. Validates plan.md / goal.json commit, spawn,
iter 0 ledger artifacts (directive + eval_output + diff.patch +
verdict), and termination on target_hit. Cost ~$1-3, wall-clock
~5-15 min. Run with npx tsx scripts/smoke-autoloop.ts (requires
~/.claude/settings.json to have your auth env).
webchatchannel is a no-op —notifyUserFallbackChaindoes not yet carry a webchat session id at the run level, sochannel: 'webchat'always returnschannel_used: 'none'. Useauto/wechat/emailuntil the inbound route lands.- One-way push. WeChat → Planner inbound replies are not yet wired (would
need an openclaw-gateway tmux-passthrough route). Reply via webchat /
autoloop_chat. - No webchat UI yet. Backend SSE is shipped; the UI is a separate cross-repo PR in ChatGPT-Next-Web.
- No fork / population mode. Single linear iter trajectory per run.
- Cross-run knowledge isolated. Each run's
reviewer_memory.mdandcoder_notes.mdlive in that run's ledger; no shared meta-store yet. - No cost / wall-clock budget cap. Only
phaseErrorCircuit+ Reviewer hold/reject streaks bound the run; a steady-but-pointless ratchet could run for days. Setmax_itersingoal.jsonto bound iter count. - Run state in memory. SessionManager restart drops the live
autoloopsmap; the on-disk ledger survives but cannot resume a running state. - Multi-run / same workspace races on
git index.lock. Run separate workspaces (or git worktrees) for concurrent runs.
{ "scalar": { "name": "test_pass_rate", "direction": "max", "extract_cmd": "bash eval.sh | grep -oE 'metric=[0-9.]+' | cut -d= -f2", "target": 1.0 }, "gates": [ { "name": "tests_pass", "cmd": "npm test", "must": "exit-0" } ], "termination": { "max_iters": 10, "scalar_target_hit": true } }