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 commands often need autonomous iteration — crank through a plan, re-run a review
until clean, drive a refinement to convergence. An earlier external integration with the
ralph-loop plugin handled this, but two failure modes kept surfacing: re-entry conflicts
between an external stop hook and Synthex's internal review loops produced stalls and
double-execution, and a single shared state file blocked two Claude sessions on the same
project from looping concurrently.
Native looping replaces it. Iteration runs internal to the command, 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. The external ralph-loop integration was retired in
synthex 0.9.8.
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. 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 50Solo 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)