Run a Ralph loop

Re-feed the goal up to N times until the agent emits STOP on its own line — with stall detection so a no-op iteration ends the run early.

When to use it

The Ralph Loop is an outer wrapper on the autonomous loop. It re-feeds the same goal across iterations — useful when one pass isn't enough but you don't want to sit and prompt the agent manually. Three concrete shapes:

Don't use Ralph for read-only or one-shot questions. An audit that finishes in one pass should not run a second. Setting the cap too high turns into a token-burn machine.

How to enable it

$ cantrip run --print "Charm this Flask app" --ralph 5
$ cantrip run --print --json --yolo --ralph 10 "Pack and deploy"
$ cantrip run /path/to/charm
> /ralph 5
> /ralph off

--ralph N on the run subparser sets the iteration cap. In the TUI, /ralph N stamps the cap on the session for the next print-mode invocation. The slash command shows the current cap when called bare; /ralph off disables.

Three values matter:

0 (default)
Ralph disabled. Single-shot run.
N > 0
Cap at N iterations. Convergence or stall detection can end the run earlier; iteration N that doesn't converge exits with status 1 and a RALPH_EXHAUSTED event.
-1
Unlimited. Run until the agent emits the convergence signal or stall detection trips. Bounded internally by a safety ceiling of 200 iterations so a stuck agent can't run forever.

The convergence signal

Each iteration ends with the agent's response. If that response contains STOP on a line by itself or as a standalone whitespace-separated token, Ralph treats it as convergence and exits with status 0.

Examples that converge:

All tests pass.
STOP
Run finished: 12 unit tests, 4 integration tests, 0 failures.
The charm is ready to merge.

STOP

Examples that don't converge:

Tests STOPPED early due to a panic — investigating.

(STOPPED is not STOP — substring matching inside a word is deliberately disallowed so the agent can't end the loop accidentally.)

The signal is currently fixed at STOP. Future config blocks may make it user-configurable per session.

Stall detection

If iteration N produces an identical response to iteration N-1 and (when git is available) leaves the working tree in the same state — same HEAD, same git status --porcelain output — Ralph exits with status 1 and a RALPH_STALLED event. The reasoning: an iteration that didn't change the response or the files isn't refining anything; running the cap out would just burn tokens.

When the working directory isn't a git repo (or git isn't available), stall detection falls back to response-only matching. That's slightly noisier — an agent that says "I made progress on X" twice with subtly different wording won't trip the stall — but identical replies still do.

Exit codes

0
The agent emitted the convergence signal and every queued task settled into done.
1
The cap fired before convergence (RALPH_EXHAUSTED) or stall detection tripped (RALPH_STALLED) or any task ended in failed / blocked. Ralph treats "didn't actually finish" as a failure for CI purposes.

Lifecycle events

Under --print --json, four event types frame the loop in the NDJSON stream:

ralph_iteration_started
Fires at the top of each pass with iteration, max_iterations (or null for unlimited), and the original goal verbatim.
ralph_converged
Fires when the convergence signal is found. Carries the iteration number and the signal string that matched.
ralph_stalled
Fires when two consecutive iterations are indistinguishable. Carries a human-readable reason for the stall.
ralph_exhausted
Fires when the iteration cap fires before convergence and the run hadn't stalled. Carries the iteration count and the user-supplied cap.

The print-mode event schema documents the full payload shape.

What the agent sees on iteration N

Iteration 1 receives the user's goal verbatim. Iteration N > 1 receives a re-seeded prompt that bookends the goal with status framing:

This is Ralph iteration 2.
Original goal:
Charm this Flask app

Last iteration's final response (for context):
… (truncated to 1500 chars) …

Continue refining toward the original goal.  When the goal is
complete, emit `STOP` on a line by itself to end the loop.

The original goal is preserved verbatim across iterations — the loop never re-summarises or paraphrases it, since re-summarisation tends to drift the target over multiple passes.

Composing in CI

A typical CI invocation pairs Ralph with print mode and yolo:

$ cantrip run \
--print "Pack and deploy the charm, run integration tests" \
--json \
--yolo \
--ralph 8 \
--max-iterations 50 \
/workspace/my-charm \
| tee build.ndjson \
| jq 'select(.type | startswith("ralph_"))'

The jq filter narrows the live event stream to just the Ralph lifecycle events — useful for the build log. Exit-code propagation lets the CI job decide pass/fail.

Related references