Run Change B with a runner and gates

Day 2: Loops and your environment · Exercise 1 of 4 · 30 min

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.

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

  1. Confirm the copy is green before the runner starts, so a suite it inherited red cannot be mistaken for one it broke.

    Swift lane
    swift test

    In 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 … (…) seconds

    The counts hold until the runner changes them. The skipped test is LEDGER-7.

    Kotlin lane
    ./gradlew test

    The line that decides it:

    BUILD SUCCESSFUL in …
    5 actionable tasks: …

    The task counts differ between a first run and a repeat; BUILD SUCCESSFUL does not.

  2. Read specs/daily-summary.md: four numbered criteria, two files in bounds, a list out of bounds, and a budget of 250 changed lines.

  3. In .claude/skills/run-spec/SKILL.md, find the section headed The three exits and write down the three exits and the iteration cap before anything starts.

  4. Design one gate of your own before the run starts. In the same file, the copy in day2-work and not the starter, add a fourth condition under The 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, under What 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, the The three gate conditions heading and the line One 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.md

    Type the paragraph, then Ctrl+D on its own line. The self-check reads the file.

  5. Start Claude Code in day2-work with claude, then invoke the runner with the spec path as its argument after the skill's name.

    /run-spec specs/daily-summary.md

    Do 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.

  6. 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.json at the root of the git repository; Exercise 3 shows you where that is.

  7. 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.
  8. 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.
  9. 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 lane
    swift test
    LEDGER_RUN_KNOWN_ISSUES=1 swift test

    With 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 … (…) seconds

    The 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 test

    The gated run ends in BUILD FAILED with 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.

  10. Read run-log.md from the top, then copy it where the reset cannot reach it.

    cp run-log.md ../work/e1-run-log.md
  11. 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 lane
    diff -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.md
    Kotlin lane
    diff -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.kt

    The 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.

  12. 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

Swift lane

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.

Kotlin lane

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
Swift lane
diff -ruN -x .build -x .claude -x specs -x run-log.md ../sandbox/swift . | grep '^diff '
Kotlin lane
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.

Swift lane
cd .. && rm -rf day2-work && cp -R sandbox/swift day2-work && cd day2-work
Kotlin lane
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