Run Change B with a runner and gates
Add one gate of your own to the shipped /run-spec skill, run it
against the reference Change B spec until it stops on its own, then say which of its three exits
fired, on what evidence, and, if a gate fired, what you did at it.
Setup
Paste one block from the course root. You never edit sandbox/ itself: the block
copies your lane to day2-work, installs the runner, the spec and the run-log template
in the copy, and leaves your shell inside it. It also makes work/ at the course root
for your answers. Every later command on this page runs from
inside day2-work, and the starter files are at
../day-2-loops-and-your-environment/starter/. If day2-work is already
there, remove it first with rm -rf day2-work, or run the reset at the bottom of this
page from inside it.
mkdir -p work
cp -R sandbox/swift day2-work && cd day2-work
mkdir -p .claude/skills/run-spec
cp ../day-2-loops-and-your-environment/starter/skills/run-spec/SKILL.md .claude/skills/run-spec/
cp ../day-2-loops-and-your-environment/starter/specs/daily-summary.md specs/
cp ../day-2-loops-and-your-environment/starter/run-log.template.md .
ls .claude/skills/run-spec specs
mkdir -p work
cp -R sandbox/kotlin day2-work && cd day2-work
mkdir -p .claude/skills/run-spec
cp ../day-2-loops-and-your-environment/starter/skills/run-spec/SKILL.md .claude/skills/run-spec/
cp ../day-2-loops-and-your-environment/starter/specs/daily-summary.md specs/
cp ../day-2-loops-and-your-environment/starter/run-log.template.md .
ls .claude/skills/run-spec specs
The block prints SKILL.md and then TEMPLATE.md and
daily-summary.md. A project skill lives at
.claude/skills/<name>/SKILL.md inside the project it belongs to, and a session
works in the directory you launch it from, so start Claude Code inside
day2-work.
The spec covers both lanes: one file, with the Kotlin path or command in brackets after the
Swift one. Keep a second terminal tab open in day2-work for the shell commands
below.
Budget about twelve of the 30 minutes for the run itself: an agent turn up to the gate, if one fires, and another after it, about five minutes each, counting the approval prompts, and the question at the end. Five minutes before that go on the gate you write in step 4.
Steps
-
Confirm the copy is green before the runner starts, so a suite it inherited red cannot be mistaken for one it broke.
Swift laneswift testIn a fresh copy the count line reads:
Test Suite 'All tests' passed at …. Executed 34 tests, with 1 test skipped and 0 failures (0 unexpected) in … (…) secondsThe counts hold until the runner changes them. The skipped test is LEDGER-7.
Kotlin lane./gradlew testThe line that decides it:
BUILD SUCCESSFUL in … 5 actionable tasks: …The task counts differ between a first run and a repeat;
BUILD SUCCESSFULdoes not. -
Read
specs/daily-summary.md: four numbered criteria, two files in bounds, a list out of bounds, and a budget of 250 changed lines. -
In
.claude/skills/run-spec/SKILL.md, find the section headedThe three exitsand write down the three exits and the iteration cap before anything starts. -
Design one gate of your own before the run starts. In the same file, the copy in
day2-workand not the starter, add a fourth condition underThe three gate conditions, in the shape of the three above it: one bold name, then one or two sentences saying what the runner compares and what counts as true. Pick a condition a run of Change B could trip and a command could show. Two that work: the test count in the verification command's summary line goes down between iterations, which means a test was deleted rather than repaired; or one iteration edits both in-bounds files at once, so a red result cannot say which edit caused it. Then, underWhat a gate looks like, add the question, the evidence and the two or three answers your gate would present, in the example's shape. Change “three” to “four” in two places, theThe three gate conditionsheading and the lineOne of the three conditions below became true, so the runner counts yours. Save the file: Claude Code reads it when you invoke the skill, so the gate is live from the first pass.Record which condition you chose and why, one paragraph:
cat > ../work/e1-gate.mdType the paragraph, then Ctrl+D on its own line. The self-check reads the file.
-
Start Claude Code in
day2-workwithclaude, then invoke the runner with the spec path as its argument after the skill's name./run-spec specs/daily-summary.mdDo not paste the runner's instructions into the chat by hand. The file is the procedure; typing it out again is the habit today replaces.
-
Let it run. Do not send a new instruction while it is working, and do not paste a failure back to it. What it does unsteered is the measurement.
Until Exercise 3 writes a settings file, Claude Code asks before each file edit and before commands such as the test command: default mode allows only reads. Approving is not steering: answer Yes, or allow edits for the session, and type nothing else. A “Yes, and don't ask again” is saved to
.claude/settings.local.jsonat the root of the git repository; Exercise 3 shows you where that is. -
If a gate fires, answer from the evidence in the gate message alone. The shape below is a decision, the bound restated, and an instruction to write the reason where the run can still read it after a compaction. Replace the criterion number with the one your gate names.
Continue. Criterion 4 stands as written. Do not change any file outside the bounds listed in the spec. Before you resume, append one line to run-log.md recording this decision and the reason in one sentence. -
When the run stops, make it name its own exit rather than summarise.
Print the last line of run-log.md, then tell me which stop condition ended the run: green, the iteration cap, or a gate. Quote the test output line that carries the verdict, not whatever printed last. -
Verify it yourself, in the second tab: your lane's test command, then the same with the known-issues gate set. The spec puts LEDGER-7 out of scope, so the gated run must still fail on the JPY case and nothing else.
Swift laneswift test LEDGER_RUN_KNOWN_ISSUES=1 swift testWith the reference result in place, the gated run printed this, with the path shortened and the error line wrapped to fit:
…/Tests/LedgerKitTests/KnownIssueTests.swift:35: error: -[LedgerKitTests.KnownIssueTests testLEDGER7_JPYRendersWithTwoDecimalPlaces] : XCTAssertEqual failed: ("-¥1,280.00") is not equal to ("-¥128,000") - LEDGER-7: LK-014 (JPY) rendered the wrong amount. expected: -¥128,000 actual: -¥1,280.00 … Executed … tests, with 1 failure (0 unexpected) in … (…) secondsThe count may no longer be 34, because the runner rewrote the summary tests; the verdict is 1 failure.
Kotlin lane./gradlew test LEDGER_RUN_KNOWN_ISSUES=1 ./gradlew testThe gated run ends in
BUILD FAILEDwith one assertion above it. With the reference result in place, it printed:KnownIssueTest > LEDGER-7 the JPY row renders with decimal places it should not have() FAILED org.opentest4j.AssertionFailedError: … ==> expected: <-¥128,000> but was: <-¥1,280.00> … 85 tests completed, 1 failed … BUILD FAILED in …The completed count moves with the rewritten summary tests; the verdict is 1 failed.
-
Read
run-log.mdfrom the top, then copy it where the reset cannot reach it.cp run-log.md ../work/e1-run-log.md -
Check the bounds against the shipped lane rather than the log. The first command lists every file that differs, including files the runner created; the second counts changed lines exactly the way the runner measured its budget.
Swift lanediff -ruN -x .build -x .claude -x specs -x run-log.md ../sandbox/swift . | grep '^diff ' diff -ruN -x .build -x .claude -x specs -x run-log.md ../sandbox/swift . | grep -vE '^(--- |\+\+\+ )' | grep -cE '^[+-]'With the reference result in place, the list read:
diff -ruN … ../sandbox/swift/Sources/LedgerKit/LedgerSummary.swift ./Sources/LedgerKit/LedgerSummary.swift diff -ruN … ../sandbox/swift/Tests/LedgerKitTests/LedgerSummaryTests.swift ./Tests/LedgerKitTests/LedgerSummaryTests.swift diff -ruN … ../sandbox/swift/run-log.template.md ./run-log.template.mdKotlin lanediff -ruN -x build -x .gradle -x .kotlin -x .claude -x specs -x run-log.md ../sandbox/kotlin . | grep '^diff ' diff -ruN -x build -x .gradle -x .kotlin -x .claude -x specs -x run-log.md ../sandbox/kotlin . | grep -vE '^(--- |\+\+\+ )' | grep -cE '^[+-]'With the reference result in place, the list read:
diff -ruN … ../sandbox/kotlin/run-log.template.md ./run-log.template.md diff -ruN … ../sandbox/kotlin/src/main/kotlin/ledgerkit/LedgerSummary.kt ./src/main/kotlin/ledgerkit/LedgerSummary.kt diff -ruN … ../sandbox/kotlin/src/test/kotlin/ledgerkit/LedgerSummaryTest.kt ./src/test/kotlin/ledgerkit/LedgerSummaryTest.ktThe list should be the spec's two in-bounds files and
run-log.template.md, which setup copied in. The count is at most 250, including the template's 34 lines, as in the runner's own measure. -
Open the reveals. If a gate fired, compare the runner's gate wording against the gate you hit; if none did, compare your run's exit with the reference log's.
If you see something else instead
Likely wrong turns
- Typing
/run-spec specs/daily-summary.mddoes not start the runner. The skill file is not where Claude Code looks for it. Quit, runls .claude/skills/run-specfromday2-workto confirmSKILL.mdis there, and start Claude Code again from inside it. - No
run-log.mdexists when the run stops. The runner skipped step 5 of its own loop, so the run has no memory outside the transcript, and a compaction now costs you the history. Rebuild the log before you close the session. - It asks a question that is not one of the four gate conditions in your copy. That is an interruption, not a gate. Answer in one sentence and note it: a runner that asks for reassurance has a spec that left something undecided.
- No gate fired. That is a result, not a fault: add one sentence saying so to
../work/e1-gate.md, and name the exit as green or the cap. - It stops green on the first iteration. Read the diff before you believe it: the fastest route to green is a weakened test, and the spec's grid names the assertion each criterion needs. A one-pass green is real when every test name in the grid appears in the diff and the diff deletes no existing test, so the test count did not fall; if it deletes one, check that a grid test replaced it before you believe the green.
- To start over rather than debug an edit, run the reset at the bottom of this page, after
copying
run-log.mdto../workif you want to keep it.
A line beginning error: where the count line should be means the package did
not compile, and the runner's last edit caused it. The exception is error: PCH was compiled with module cache path
'…/sandbox/swift/.build/…' before the runner has started: someone built inside
sandbox/swift, and the copy brought that build folder along. Run
rm -rf .build in day2-work and test again.
BUILD FAILED with a FAILED line naming a test class means a test
went red; open the HTML report path printed beside it.
Self-check
Four commands decide it
Run all four in the second tab.
grep -c '^| iteration ' run-log.md
grep -n '^| GATE \|^| ANSWER ' run-log.md
wc -w ../work/e1-gate.md
diff -ruN -x .build -x .claude -x specs -x run-log.md ../sandbox/swift . | grep '^diff '
diff -ruN -x build -x .gradle -x .kotlin -x .claude -x specs -x run-log.md ../sandbox/kotlin . | grep '^diff '
The first prints a number between 1 and 5: the cap is MAX_ITERATIONS = 5, and
only a pass through the loop carries the word iteration. The second prints your
gate and the answer you gave it; nothing printed means no gate fired, or you answered in the
chat and it never reached the log, the version that survives. If a GATE row
names the condition you wrote in step 4, your gate fired; that is a result, not a fault, and
the answer row should say what you did about it. The third prints a word count above zero.
The fourth names your lane's two in-bounds files and run-log.template.md; another
path is the runner leaving its bounds.
Done when: your own gate is in the copy's
SKILL.md and ../work/e1-gate.md says why you chose it, the run ended on
its own rather than because you interrupted it, run-log.md has one row per iteration
and a final line of prose naming the exit, a copy of it is in ../work/e1-run-log.md, your own test run
is green, the gated run fails on the JPY case alone, and the diff against the shipped lane names
no file the spec put out of bounds.
Reference answers
The three exits and the three gate conditions, in the runner's own words
From the shipped SKILL.md, with the Markdown emphasis and backticks
stripped and the wording untouched. Take them from the file, not from memory of Lesson 2. Your
copy has a fourth condition; it is yours, and nothing here grades it. The
exits:
- Green. The verification command exited zero and its summary line says
so, in the words the spec gives for success (Swift: the Executed … tests
line with 0 failures; Kotlin: BUILD SUCCESSFUL in …). Print that line and
the last line of run-log.md, then stop.
- The cap. MAX_ITERATIONS = 5. Iteration five is the last one you run.
If it ends red, stop anyway, print the failure, and say the cap fired.
Five passes that did not converge is information, not a reason for a
sixth.
- A gate. One of the three conditions below became true. Stop mid-repair
and ask.
The gates:
- 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. Do not widen
the bounds yourself, and do not quietly take the in-bounds route either:
offer it as an answer.
- The same failure twice, byte for byte. Compare the failure text from this
iteration with the previous one. Identical text means the repair changed
nothing the test can see, and a third attempt will not either.
- The diff exceeds the spec's line budget. Measure against the shipped
lane this copy came from (../sandbox/swift when this folder has
Package.swift, ../sandbox/kotlin otherwise), added plus deleted:
diff -ruN -x .build -x build -x .gradle -x .kotlin -x .claude -x specs -x run-log.md ../sandbox/swift . | grep -vE '^(--- |\+\+\+ )' | grep -cE '^[+-]'
(Kotlin: the same with ../sandbox/kotlin). If the spec states no budget,
treat 200 changed lines as the budget and say so at the gate.
The spec you ran states its own budget, 250 changed lines, so the third gate uses that number and not 200. Green is keyed to the summary line by what it says, not to whatever prints last, because in both lanes a banner prints after it; that line is the evidence step 8 asks for. The file also says what a gate is made of: the question, the evidence quoted rather than described, and two or three answers you can say back in a sentence. A stop that is missing the evidence is not a gate, and answering it is guessing.
A reference run log: four iterations, one gate, exit green
This is
../day-2-loops-and-your-environment/starter/reference/after-e1/run-log.md, a
reference log in the layout this skill uses, not a recording of a run. It names the Swift lane's
files, and its title and table header are left out here.
Exercise 2 shows that the implementation shipped beside it does not do everything the log says.
Your own log will differ. What should match is the shape: one row per pass, a
GATE row with its ANSWER beneath it, and a final line of prose naming
the exit.
| iteration 1 | added CurrencyTotal, replaced DaySection.totalMinor with totals, left byDay alone | build failed: LedgerSummaryTests references totalMinor in 2 places | that file is in bounds; rewrite its two assertions |
| iteration 2 | rewrote LedgerSummaryTests against the new shape; byDay still groups and sums the old way | 3 failures: declined counted, no pending figure, days oldest first | implement the grouping and the totals properly in byDay |
| iteration 3 | byDay now splits posted and pending per currency and reverses the day order | 1 failure: the totals order is not stable across runs | sort the currency list; see the gate below |
| GATE | — | criterion 4 asks for currency-code order; the tidiest home for a comparator is Sources/LedgerKit/Transaction.swift, which the spec lists as out of bounds | stopped and asked: A widen the bounds, B keep the sort private inside LedgerSummary.swift, C stop and re-scope |
| ANSWER | — | B | keeping it private costs four lines and touches no file outside the bounds; a shared comparator is a separate change with its own spec |
| iteration 4 | sorted the per-currency totals inside LedgerSummary.totals, no new file | Executed 32 tests, with 1 test skipped and 0 failures | green |
Exit: green, on the fourth pass. `diff -ruN` against `../sandbox/swift` counted
171 changed lines, under the spec's 250: the two in-bounds files and the 34 lines
of `run-log.template.md`, nothing else.
The gate in this log is the first one: the better home for the change is a file the spec fenced off, even though an in-bounds route exists, and the runner is forbidden from widening the fence itself or settling the trade-off for you. Answer B is the ordinary answer. It is worth noticing what the log records about it, which is not the decision but the reason.
If the gate you wrote in step 4 is the falling test count, it would have fired on this log too: iteration 4 reports 32 tests where step 1 printed 34, and nothing in the log says which two went. That stop is the gate doing its job. The answer starts with reading the diff for the deleted tests and checking that each criterion still has its assertion.
The gate almost nobody reaches is the second, the byte-identical repeat failure. It needs two consecutive passes whose failure text matches exactly, and most people interrupt the run somewhere in the first of those two passes, because watching a repair fail twice is uncomfortable and typing something feels like helping. It is not: a new instruction resets what the runner is comparing against, and the gate that would have told you the repair loop was stuck never fires. If your run ended because you typed, record that as the exit. It is the most useful result this exercise produces.
Reset
Every Day 2 exercise resets the same way: quit Claude Code, then from inside
day2-work run your lane's line. It replaces the copy with a fresh one from the
shipped lane and leaves you inside it; sandbox/ and your answers in
../work/ are untouched.
cd .. && rm -rf day2-work && cp -R sandbox/swift day2-work && cd day2-work
cd .. && rm -rf day2-work && cp -R sandbox/kotlin day2-work && cd day2-work
A fresh copy is the shipped lane and nothing else, so re-run this page's setup before you
continue: from inside the new copy, paste the setup block's lines after
cd day2-work. run-log.md goes with the old copy, which is why you copied
it into ../work/.
Do not reset when you have finished: Exercise 2 audits the implementation this run leaves in
day2-work. Reset only to run this exercise again.
Sources
cc-automation-01,cc-automation-03— a project skill lives at.claude/skills/<skill-name>/SKILL.mdand is invoked by name with arguments after it, from Agent Skills.cc-core-41— the directory you launch Claude Code in is the session's primary working directory, from Configure permissions, accessed 2026-09-23.cc-core-07— default mode allows reads only; file edits and commands need approval, from Permission modes.cc-core-39— “Yes, and don't ask again” saves the rule to.claude/settings.local.jsonat the root of the git repository, and a file-edit approval lasts only for the session, from Configure permissions.cc-core-35— automatic compaction replaces the conversation with a structured summary, from Explore the context window.cc-core-32— the version this exercise was checked against, Claude Code 2.1.280, from the Claude Code changelog.- The quoted test and diff output was recorded on this machine on 2026-09-23 in a fresh copy
made by this page's setup block, one run per lane; where it says so, with
../day-2-loops-and-your-environment/starter/reference/after-e1/copied in to stand for the runner's result. The runner, the spec and the reference run log ship verbatim in../day-2-loops-and-your-environment/starter/.