CONCEPTS · NATIVE LOOPING

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:

  1. Boundary check. Read the state file. If status is anything but running, exit. If iteration >= max_iterations, mark max-iterations-reached and exit.
  2. 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.
  3. Print a short marker to stdout: [loop <loop-id> iteration N/M].
  4. 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.
  5. Scan the iteration's final response for the literal <promise>X</promise> XML tag (same convention as Ralph). On match: mark completed, exit.
  6. 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-last

Resume 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 50

Coexistence 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-1

next-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-loops

Need to stop early?

/synthex:cancel-loop release-1

Cancellation 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 10

The 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. --loop never edits .synthex/config.yaml or 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-priority parallelises 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)