The runner: non-stop until a stop condition, with limited gates

Day 2: Loops and your environment · Lesson 2 of 4 · 12 min

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.

Watch first · 3 min

Why Software Factories Fail

AI Engineer, 2026-07-23. Segment 5:52 to 8:56, two chapters: the lights-off factory, where the agents build and nobody reads the code, then the speaker's own July 2025 experiment with one, which fell apart on an issue no amount of prompting could fix, in code nobody had read for three months. Watch for the absences rather than the failures: what nobody was looking at, and the point at which someone would have had to be asked a question. Hold that point in mind, because it is what the rest of this lesson calls a gate. His fix, plan up front and turn review back on, is the closing chapters from 14:58, which you watched in Day 1's Lesson 4.

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 lane
swift test
Kotlin lane
./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.

The runner loop and its three exits Time runs left to right. The spec feeds implement, implement feeds test, and a dashed return arrow labelled repair runs below those two boxes from test back into implement; that return is the only loop in the drawing. A run leaves the loop by exactly one of three exits drawn to the right of test: green, meaning the tests exited zero; cap, meaning iteration 5 ended red; and gate, meaning one question that stops the run. The gate is drawn in the accent colour because it is the exit that comes back to you. One row per pass is appended to run-log.md when the test command returns. spec implement test repair green tests exited zero cap iteration 5 ended red gate one question, stop one row per pass appended to run-log.md
One loop, three ways out: the exit that fires decides what you do next.

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.

Swift lane
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.
Kotlin lane
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

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:

Swift lane

.build/, the only folder swift test adds.

Kotlin lane

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.