Native looping
Pass --loop to a Synthex command (or invoke
loop/synthex:loop with a free-form prompt) and the command re-runs itself
until a completion promise is emitted or a max-iteration cap is hit — all in the same
agent thread, with state persisted to disk so two sessions on the same project never collide
and a closed laptop never loses progress.
Why it exists
Synthex used to integrate with the official ralph-loop plugin for autonomous iteration:
configure a completion promise, set max iterations, and let the agent crank through a plan.
That worked, but two failure modes kept surfacing.
Combining ralph-loop with next-priority/synthex:next-priority produced bugs. The Ralph
plugin owns a stop hook that re-injects the prompt into a fresh agent every iteration.
Synthex's command, in turn, has its own internal review loops and milestone gates. The
interaction between the two re-entry models caused stalls, double-execution of tasks, and
occasional missed completion signals.
Two Claude sessions on the same project couldn't both loop. Ralph keeps state in a single
.claude/ralph-loop.local.md. Open the project in a second session and either you collide
with the first session's loop or you can't start a new one. There was no way to run two
independent loops — one autonomous build, one targeted refinement, say — concurrently.
Native looping fixes both. Iteration runs internal to the command (no stop hook), state is
per-loop in .synthex/loops/<loop-id>.json (concurrent loops just have different IDs), and
the framework is part of Synthex itself so the command's own behavior and the iteration
behavior are designed together.
How it works
When you pass --loop, the command's markdown instructs the agent to run its
normal workflow inside a tight outer loop:
- Boundary check. Read the state file. If
statusis anything butrunning, exit. Ifiteration >= max_iterations, markmax-iterations-reachedand exit. - Increment + persist the iteration counter, before doing iteration work. That's the durability boundary — a crash here costs one iteration of work, never the counter.
- Print a short marker to stdout:
[loop <loop-id> iteration N/M]. - Run the command's existing workflow unchanged. The command writes its output to your plan, your review report, or whichever artifact it already owns — never to the conversation.
- Scan the iteration's final response for the literal
<promise>X</promise>XML tag (same convention as Ralph). On match: markcompleted, exit. - Re-read the state file. If another session set
status: cancelled, exit. Otherwise loop.
The whole thing runs as a single Claude Code agent thread. Claude's built-in auto-compaction handles the context window — see Auto-compaction for why that's safe.
If you'd rather isolate each iteration (longer plans where context bleed across iterations
actively hurts), pass --loop-isolated. Each iteration then spawns a fresh sub-agent via the
Task tool; the outer command marshals the loop counter and promise scan.
Multi-session
Loop state lives at <project>/.synthex/loops/<loop-id>.json — one file per loop, never
shared. Two sessions on the same project run independent loops with different IDs and write
to different files. list-loops/synthex:list-loops enumerates everything in the directory
so each session can see what the other is doing.
Loop IDs come from --name <slug> (whatever you want, lowercase + hyphens, ≤ 64 chars) or
auto-generate as <command>-<4-char-hex>. The auto-naming uses crypto-strength random bytes,
so collisions are vanishingly rare — and the framework retries on collision regardless.
Auto-compaction
The iteration loop runs in a single agent thread, so Claude Code's auto-compaction will fire when the conversation grows large. Native looping is designed to survive that without losing its place. Three rules make it safe:
- State lives on disk, not in conversation. The iteration counter, the completion-promise
text, the args — every durable bit is in
.synthex/loops/<loop-id>.json. Compaction can summarize the conversation arbitrarily; the state file is untouched. - Iteration work lives in your artifact, not in conversation. Each iteration writes its output to the implementation plan, the review report, the PRD — wherever the command already writes. The next iteration reads from disk, not from prior chat history.
- Markers are short. The
[loop <loop-id> iteration N/M]line is one line. It survives compaction summaries verbatim, so the agent always knows where it is.
If compaction does erase the loop-id from working memory, the recovery rule is in the spec:
list .synthex/loops/, find the running file matching this command, continue. If multiple
match, the agent exits and tells you to resume explicitly.
Resume across sessions
Closing Claude Code mid-loop or losing power doesn't end the work — it pauses it. The state file persists. From a fresh session:
# Resume by id
/synthex:loop --resume my-loop-name
# Resume the most-recent running loop in this project
/synthex:loop --resume-lastResume re-uses the persisted command, args, prompt, and completion promise — you don't
re-supply them. If multiple loops are running, --resume-last prefers the one whose
session_id matches your current session; if none match, the most-recently-touched.
A loop reaches max-iterations-reached and you want more? Resume with a bigger cap:
/synthex:loop --resume my-loop-name --max-iterations 50Coexistence with Ralph Loop
Native looping does NOT replace the official ralph-loop plugin. If you have Ralph
configured and prefer its workflow, keep using it. The Ralph Loop Integration sections in
next-priority/synthex:next-priority and /synthex-plus:team-implement remain functional.
When both are active — Ralph plugin running AND you pass --loop — native takes precedence.
The command prints a one-line advisory:
Note: --loop overrides Ralph Loop. The ralph-loop plugin's state file is unchanged;
cancel the ralph loop separately if you want it gone.The Ralph plugin's state file is never mutated by native looping. Cancel the Ralph loop the usual way (through the Ralph plugin) if you want it gone.
Solo example: loop a plan to completion
/synthex:next-priority --loop \
--completion-promise "ALLDONE" \
--max-iterations 30 \
--name release-1next-priority/synthex:next-priority iterates: pick the next batch of unblocked tasks, spin
up worktrees, delegate to Tech Lead instances, merge, mark done in the plan, repeat. The loop
exits when every task across every milestone is done — at which point the command emits
<promise>ALLDONE</promise> and native looping marks the state file
completed.
Mid-run? Check on it from a second session:
/synthex:list-loopsNeed to stop early?
/synthex:cancel-loop release-1Cancellation is polled at the iteration boundary — worst-case latency is one iteration's worth of work before the loop exits cleanly.
Teams example: loop a team-review until clean
/synthex-plus:team-review --loop \
--completion-promise "REVIEW_CLEAN" \
--max-iterations 10The review team runs once per iteration: spawn → reviewers fan out → consolidate → check for
open critiques. The loop terminates when the team's consolidated report has zero FAIL
findings, zero pursued WARNs, and no recommended-change items left.
Team-specific rule (lead-output-only scan). Only the Pool Lead's consolidated output is
scanned for the promise. Individual reviewer reports are not promise sources — a single
reviewer emitting <promise>REVIEW_CLEAN</promise> in their own report does
NOT terminate the loop. The lead's final consolidation is the canonical signal.
Team lifecycle is unchanged. --loop doesn't spawn or tear down teams differently. Each
iteration uses the command's existing team lifecycle (pool reuse by default for performance;
--loop-isolated doesn't change filesystem-level state — only conversation-level).
What --loop does NOT do
A few intentional non-features, called out so you don't expect them:
- It doesn't bypass
[H]gates. Commands that wait for user input (interactive review, AskUserQuestion prompts) still wait — the loop just re-runs the command until those gates resolve naturally. Use cancel-loop if you change your mind. - It doesn't auto-mutate config.
--loopnever edits.synthex/config.yamlor anything outside the loop's own state file. - It doesn't change team lifecycle. Each iteration of a team command reuses or tears down the team per the command's existing semantics.
- It doesn't auto-reset dismissed flags. A loop respects whatever the project's existing settings dictate — it's an iteration wrapper, not a configuration override.
- It doesn't push past the 200-iteration ceiling. That cap is a runaway-protection
guarantee. If you need more, resume with
--max-iterations <N>higher; this is by design to force a deliberate decision rather than letting a misconfigured loop run forever.
See also
- loop/synthex:loop — generic looping primitive (reference)
- list-loops/synthex:list-loops — enumerate loops (reference)
- cancel-loop/synthex:cancel-loop — cancel a loop (reference)
- next-priority/synthex:next-priority — autonomous plan execution with
--loop(reference) - Parallel execution — how
next-priorityparallelises within a single iteration; native looping is the outer loop - Lifecycle — Synthex's five-phase delivery loop, of which native looping is the build-phase iteration primitive
- Upstream framework spec —
plugins/synthex/docs/native-looping.md(state schema, complete iteration loop, FR-NL identifiers)