name: loop-runner description: > Set up and explain the autonomous Ralph loop — an in-session Stop hook that re-feeds the project's iteration contract until every spec is satisfied, a UI-first gate is reached, or max iterations is hit. Triggered by /do-it:ralph. Adapted from anthropics/claude-code's ralph-wiggum plugin (the in-session Stop-hook loop) over snarktank/ralph's prompt-as-program discipline. version: 0.2.0
Loop Runner — in-session autonomous loop setup
The interactive commands (/do-it:build, /do-it:review, /do-it:iterate) are great for steering. The Ralph loop is for walking away. It runs inside the current Claude Code session: a bundled Stop hook intercepts the session's exit and feeds the same iteration contract back, so the model keeps building task-by-task until it emits a stop token or hits the iteration ceiling.
This is not an external bash loop and not claude --print. Everything happens in-session. The loop is driven by:
hooks/hooks.json+hooks/stop-hook.sh— the bundled Stop hook (ships with the plugin)..claude/do-it-ralph.local.md— the state file that activates the hook. While it exists, the loop is live; when it's removed, the session exits normally.
What this skill does
- Verifies the project is ready for the loop.
- Writes
.claude/do-it-ralph.local.md— YAML frontmatter (iteration counter, ceiling, stop tokens) followed by the substituted iteration contract as the body. - Hands control to the loop: the calling command begins iteration 1, and the Stop hook drives every iteration after that.
The contract template lives at this skill's scripts/PROMPT.md. The runner that used to live here (scripts/doit.sh) is gone — the Stop hook replaces it.
Process
Step 1: Preflight
Check that the project is ready. Refuse to start the loop if any of these are missing:
.do-it/specs/_meta.jsonexists and parses (hasproject_type,project_name).- There is something to iterate over: at least one open change in
.do-it/changes/(excludingarchive/), or a clean post-grillbootstrap state (the contract's bootstrap step will create change folders on iteration 1). - For software projects: the
branch_namefrom_meta.jsonexists as a git branch, or the user has explicitly approved creating it. - No open PRD questions block the currently-pending work.
If any check fails, list what's missing and how to fix it. Don't proceed.
Step 2: Resolve max iterations
Order of precedence: $1 argument → _meta.json.max_iterations_default → 20. Refuse values > 100 (sanity ceiling). 0 means unlimited (opt-in; warn the user it has no automatic stop other than a token).
Step 3: Materialize the contract
Read ${CLAUDE_PLUGIN_ROOT}/skills/loop-runner/scripts/PROMPT.md and substitute, reading values from _meta.json and the repo:
{PROJECT_NAME}{PROJECT_TYPE}—softwareorgeneric{BRANCH_NAME}— for software projects{CWD}— the absolute path of the project root
Step 4: Write the state file
Write .claude/do-it-ralph.local.md (create .claude/ if absent):
---
active: true
iteration: 1
max_iterations: <resolved>
completion_promise: "COMPLETE"
pause_promise: "PAUSE"
started_at: "<UTC ISO-8601>"
project: "<PROJECT_NAME>"
branch: "<BRANCH_NAME or empty>"
---
<the substituted PROMPT.md contract>
This file is the only thing the hook reads. Frontmatter keys must stay exactly as named — stop-hook.sh greps for iteration:, max_iterations:, completion_promise:, pause_promise:.
Step 5: Brief the user, then begin
Tell the user:
Ralph loop active — iteration 1 of {MAX_ITERATIONS}.
It runs in THIS session. After each turn, the Stop hook feeds the same contract
back and I work the next task. I stop automatically when:
• every spec is satisfied → <promise>COMPLETE</promise>
• a UI-first gate is reached → <promise>PAUSE</promise> (then run /do-it:review)
• the iteration ceiling is hit → {MAX_ITERATIONS}
Cancel any time with /do-it:cancel-ralph (or delete .claude/do-it-ralph.local.md).
Watch progress in .do-it/progress.md. Context accumulates across iterations; for
very long runs, cancel, /clear, and re-run /do-it:ralph to reset.
Then start iteration 1 immediately — read the on-disk context, pick the next task, build it under the build-loop discipline, self-review, apply the verdict. When the turn ends, the hook takes over.
Step 6: Do not hand-roll an external loop
The old external runner (doit.sh + claude --print) is removed. Don't re-introduce it, and don't shell out to claude from inside the session. The Stop hook is the loop.
How the hook works (for users who want to know)
On every Stop event, hooks/stop-hook.sh:
- Returns immediately (normal exit) if
.claude/do-it-ralph.local.mdis absent. - Stops the loop if
iteration >= max_iterations(max > 0). - Reads the last assistant message from the session transcript and extracts the first
<promise>…</promise>.- matches
completion_promise(COMPLETE) → remove state file, exit; project done. - matches
pause_promise(PAUSE) → remove state file, exit; human runs/do-it:review.
- matches
- Otherwise: bump
iteration, emit{"decision":"block","reason":"<contract>"}to block the stop and re-feed the contract.
Key properties:
- In-session. No separate process; the model keeps its tools and permissions. Carry knowledge forward by writing to disk (
progress.md,AGENTS.md,docs/, git) — context may be compacted between iterations. - Builds continuously; the hook is a backstop. The contract has the model build task after task within the same turn (each in a fresh
Tasksubagent so context stays lean) — it does not stop after one task. The Stop hook only re-feeds the contract if a turn is cut short, and it fails safe: on a transient transcript/parse hiccup it keeps the state file and pauses (resume with/do-it:ralph) instead of deleting it and silently killing the loop. - Runs unattended; the human checkpoint is
/do-it:grill. Three_meta.jsonkeys (resolved at launch) control it:review_mode(auto= the loop runs/do-it:reviewitself and continues;manual= pause at each gate),safety_stops(true= pause before destructive/irreversible actions;false= stop only for grill/complete/max), andautonomy_level(how ambiguity routes to grill). On a pause the model writes a### Decision needed/### Needs grillblock toprogress.mdand asks; you act and resume. - Two clean exits.
COMPLETE(done) andPAUSE(gate) are distinct, so the loop can stop for a human without lying that it's finished. This is an improvement over the old single-token external loop, which couldn't halt at a gate. - Hard ceiling.
max_iterationsis the backstop. Nowhile trueunless the user explicitly sets0. - Ungame-able stop. Literal-string promise match. The model can't pattern-match its way out.
Optional hardening
Not enabled by default. If the user asks, this skill can also:
- Archive
.do-it/progress.md+ the active change to.do-it/archive/YYYY-MM-DD-{branch}/whenbranch_namechanges between runs. - Add a circuit breaker: if
git diffis empty for N consecutive iterations, write a no-progress note toprogress.mdand emit<promise>PAUSE</promise>so a human steps in.
Suggest these for long autonomous sessions (> 25 iterations) or branch-heavy work.
Stop signal contract
The build-loop and review-work skills are responsible for emitting <promise>COMPLETE</promise> when (and only when) every requirement in .do-it/specs/ is implemented and no open changes remain, and <promise>PAUSE</promise> when a UI-first gate is reached. The hook doesn't compute either — it pattern-matches the literal tag. The loop's discipline is entirely encoded in the contract.
This means the prompt is the program. Treat changes to scripts/PROMPT.md (and the materialized state-file body) as code changes — diff them, review them, test on a low-iteration run first.
Anti-patterns
- ❌ Re-introducing an external
doit.sh/claude --printrunner. The Stop hook is the loop now. - ❌ Starting the loop with no
.do-it/specs/and no open changes — it has nothing to iterate over. - ❌ Hand-editing
.claude/do-it-ralph.local.mdfrontmatter (corrupts the counter; the hook will bail). - ❌ Removing the iteration ceiling.
- ❌ Editing the contract to drop the stop-token requirement.
- ❌ Emitting
<promise>COMPLETE</promise>or<promise>PAUSE</promise>to escape the loop when the statement isn't true.