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:
- "Make this charm pass
make check." The agent often needs to fix one error, run tests, see the next, and iterate. Ralph automates the loop. - Build-test-fix cycles in CI. Pair with
--print --yolofor a non-interactive run that refines until tests pass or the cap fires. - Multi-step refactors. "Migrate this charm from reactive to ops" rarely converges in a single conversation; Ralph gives you a bounded "keep trying" primitive.
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
Niterations. Convergence or stall detection can end the run earlier; iterationNthat doesn't converge exits with status1and aRALPH_EXHAUSTEDevent. -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 infailed/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(ornullfor unlimited), and the originalgoalverbatim. ralph_converged- Fires when the convergence signal is found. Carries the
iteration number and the
signalstring that matched. ralph_stalled- Fires when two consecutive iterations are
indistinguishable. Carries a human-readable
reasonfor 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
- cantrip run —
flag list including
--ralph N. - Print-mode event schema — payload shape for every event type Ralph emits.
- Run a single goal non-interactively — the print mode that Ralph composes with.
- Run Cantrip unattended
—
--yolo, the natural companion for Ralph in CI.