The runner: non-stop until a stop condition, with limited gates
A runner is a procedure the agent follows on its own: implement the spec, run the tests, repair what is red, and keep going without stopping to chat. One iteration cap and a small, named set of gates make that acceptable, because you know before it starts what will stop it.
What a runner is
A runner is a skill: a Markdown file at
.claude/skills/<skill-name>/SKILL.md that you invoke by name with an
argument.
/run-spec specs/daily-summary.md
The argument is the spec's path. The body is the procedure: read the spec and its in-bounds
files, make the smallest change that meets an unmet criterion, run the spec's verification
command, log the result, and decide whether to go again. Nothing on this page needs running:
Exercise 1 makes day2-work, the copy of your lane at the course root, installs the
runner and the Change B spec into it, and runs what you see here. The shipped
sandbox/ is never edited.
Three ways a run ends, and only three
Green, the iteration cap, or a gate. Named before the first iteration, each exit hands you a different next action: ship and review, find out why five passes were not enough, or answer one question.
Green means the spec's verification command, your lane's test command, exited zero:
swift test
./gradlew test
The cap is MAX_ITERATIONS = 5 in the skill Exercise 1 runs: if iteration
five ends red, the runner stops, prints the failure, and says the cap fired. Without a cap, a
runner can spend an afternoon repairing the same assertion.
What a gate is
A gate is three things: a question answerable in one sentence, the evidence to answer it in under a minute, and two or three answers to choose from. “Is this okay?” is none of them. It sends you into the code to invent the options, the work the runner was meant to spare you.
GATE: criterion 4 asks for the totals in currency-code order, and the tidiest
home for that comparator is Sources/LedgerKit/Transaction.swift, which the spec
lists as out of bounds.
Evidence:
the failure that sent me there, quoted whole: the totals order is not
stable across runs, from this iteration's test output
Answers:
A. Widen the bounds to include Transaction.swift.
B. Keep the sort private inside LedgerSummary.swift.
C. Stop here; I will re-scope the spec.
GATE: criterion 4 asks for the totals in currency-code order, and the tidiest
home for that comparator is src/main/kotlin/ledgerkit/Transaction.kt, which the
spec lists as out of bounds.
Evidence:
the failure that sent me there, quoted whole: the totals order is not
stable across runs, from this iteration's test output
Answers:
A. Widen the bounds to include Transaction.kt.
B. Keep the sort private inside LedgerSummary.kt.
C. Stop here; I will re-scope the spec.
That is the shipped runner's example in your lane's paths, and the gate Exercise 1's reference log records; illustrative, not captured from a run. A real gate quotes that failure whole, never describes it: an error you must go and find costs the minute the gate existed to save.
The three gates that ship with the starter runner
- Bounds gate: a file outside the spec's bounds must change, or is the better home for the change, including a test file the spec does not list. The runner neither widens the bounds nor quietly takes the in-bounds route; it offers that route as an answer.
- Repeat-failure gate: the same failure occurs twice, byte for byte.
- Line-budget gate: the diff exceeds the spec's line budget: 250 changed lines in the Change B spec, 200 when a spec names none.
The repeat-failure gate is the one people leave out, and it saves the most time: a repair loop that cannot change the failure text is repeating, not converging. The bounds gate stops the runner reaching green by editing files you never approved: widening the bounds is your decision.
The loop's memory is the filesystem
The runner appends one row to run-log.md when the test command returns: the pass
number, the change, the test result, the next decision. A gate adds a row, and your answer
another.
That file is the run's memory; the conversation is not. A compaction replaces the conversation
with a structured summary, /clear empties it, and tomorrow's session never had it.
All three keep the file, so a /clear between attempts costs nothing you needed.
Attach the run log to the pull request beside the diff.
Re-running it on an interval
Preview, not needed for Exercise 1.
/loop re-runs a prompt in the session you are in —
/loop 5m check if the deployment finished and tell me what happened — or a
skill, so /loop 20m /review-pr 1234 puts one on a timer. A recurring loop expires
seven days after creation, firing once more before deleting itself. Esc stops a
self-paced loop (one started with no interval) while it waits for the
next iteration.
Three surfaces repeat work, and the floor is where they differ. /loop and Desktop scheduled tasks go down to one
minute; cloud Routines, managed with /schedule or its alias /routines,
have a one-hour minimum.
Orchestration is the same loop one level up
Preview, not needed for Exercise 1.
A main session can dispatch a
subagent
per acceptance criterion and a separate reviewer, then gates once. A
subagent is a Markdown file with YAML frontmatter under .claude/agents/.
tools: narrows what the role may touch (tools: Read, Glob, Grep for an
auditor that must not write); leaving it out inherits everything. model: picks the
role's model. Write it explicitly: Lesson 3 covers its values and why omitting it undoes the
arrangement.
That is loop economics: a cheap worker and an expensive reviewer often beat one expensive
everything. Claude Code ships built-in subagents, Explore and
Plan among them, so check whether the role already exists.
When not to loop
Loop only where a command can tell you that you are done. If a criterion is a judgement call (the API reads naturally, performance is acceptable), the runner converges on something green and wrong, because green is its only signal. Send those criteria back to the spec first.
A loop also needs an undo, and day2-work has one without a repository. The clean
tree is the shipped lane, so a recursive diff against it lists every file the runner touched. It
skips the runner's own files (.claude/, specs/ and
run-log.md) and your lane's build output:
.build/, the only folder swift test adds.
build/, .gradle/ and .kotlin/, the folders
Gradle and the Kotlin compiler write.
Diff the spec and the runner against their starters too, because a runner stuck on red is
most tempted to loosen its own spec. From inside day2-work, in either lane, these
print nothing while both are untouched:
diff ../day-2-loops-and-your-environment/starter/specs/daily-summary.md specs/daily-summary.md
diff ../day-2-loops-and-your-environment/starter/skills/run-spec/SKILL.md .claude/skills/run-spec/SKILL.md
If either diff names a change you did not approve, the rollback is a fresh copy of the shipped lane. Exercise 1 gives the lane diff in its self-check and the reset at its end.
Claude Code also keeps checkpoints, reachable with /rewind or Esc
twice on an empty prompt. Treat them as a convenience on top of the reset, not the undo.
Quick check
Your runner is on iteration four of the same failing assertion. Which stop condition should have fired earlier, and what would it have put in front of you?
The repeat-failure gate, which trips when a failure recurs byte for byte. It would have shown you the identical failing output twice alongside the diff that failed to change it, which is the evidence that repair is not converging. The cap would have caught it too, but only when iteration five ended red.
Why does the runner write each iteration to a file instead of relying on the conversation it is already having?
Because the run may outlive the context it started in. A file survives compaction, a
/clear, and tomorrow's session; the transcript does not, and a loop whose memory can
evaporate cannot tell you what it already tried.
Sources
Claude Code documentation, all accessed 2026-09-22 against version 2.1.280.
cc-automation-01,cc-automation-03— the project skill path.claude/skills/<skill-name>/SKILL.mdand invoking a skill by name with an argument, from Agent Skills.cc-automation-18,cc-automation-19,cc-automation-20,cc-automation-21,cc-automation-23—/loopon a prompt and on a skill, the seven-day expiry, Esc to stop a self-paced loop (no interval given) while it waits, and the one-minute floor for/loopand Desktop tasks against the one-hour floor for cloud Routines, from Scheduled tasks.cc-automation-24—/scheduleand its/routinesalias for creating and managing a cloud Routine, from Routines.cc-automation-27—/rewindand Esc Esc on an empty prompt, from Checkpointing.cc-core-19—/clearand its aliases, from Slash commands.cc-core-35— compaction replaces the conversation with a structured summary, from The context window.cc-core-25,cc-core-26,cc-core-27,cc-core-30— subagents as frontmatter plus Markdown under.claude/agents/, themodel:andtools:fields, and the built-in subagent types includingExploreandPlan, from Subagents.cc-core-32— the version this course was checked against, from the Claude Code changelog.