Make it critique itself

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

Put a fresh-context auditor on the green result your runner produced, route every finding it returns into the spec or your standing instructions without growing either, then re-run from a fresh copy and see whether the change paid for itself.

Setup

This exercise continues in day2-work, the copy Exercise 1 made at the course root, against the implementation and run-log.md that run left, so do not reset in between; the shipped sandbox/ is never edited. Quit Claude Code and paste one block from the course root (cd .. first if you are in the copy). It installs the auditor, the runner (keeping the one you edited in Exercise 1 if it is already there) and the spec, saves your starting CLAUDE.md line count to ../work/, and ends inside day2-work, where every later command runs. Your notes go in ../work/e2.md.

Swift lane
mkdir -p work && { [ -d day2-work ] || cp -R sandbox/swift day2-work; } && cd day2-work && mkdir -p .claude/agents .claude/skills/run-spec && cp ../day-2-loops-and-your-environment/starter/agents/spec-auditor.md .claude/agents/ && { [ -f .claude/skills/run-spec/SKILL.md ] || 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/ && wc -l CLAUDE.md > ../work/e2-swift.txt && ls .claude/agents/spec-auditor.md specs/daily-summary.md
Kotlin lane
mkdir -p work && { [ -d day2-work ] || cp -R sandbox/kotlin day2-work; } && cd day2-work && mkdir -p .claude/agents .claude/skills/run-spec && cp ../day-2-loops-and-your-environment/starter/agents/spec-auditor.md .claude/agents/ && { [ -f .claude/skills/run-spec/SKILL.md ] || 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/ && wc -l CLAUDE.md > ../work/e2-kotlin.txt && ls .claude/agents/spec-auditor.md specs/daily-summary.md

The saved count is what the net-zero rule holds you to: your instructions may get better, not longer.

If your Exercise 1 run did not finish, also paste this catch-up block from inside day2-work. It copies in a finished run whose own tests pass and which is wrong in six places, with its log.

Swift lane
cp -R ../day-2-loops-and-your-environment/starter/reference/after-e1/swift/. . && cp ../day-2-loops-and-your-environment/starter/reference/after-e1/run-log.md . && ls Sources/LedgerKit/LedgerSummary.swift
Kotlin lane
cp -R ../day-2-loops-and-your-environment/starter/reference/after-e1/kotlin/. . && cp ../day-2-loops-and-your-environment/starter/reference/after-e1/run-log.md . && ls src/main/kotlin/ledgerkit/LedgerSummary.kt

Start Claude Code inside day2-work with claude, and keep a second terminal tab there for the shell commands.

Steps

  1. Read the auditor's frontmatter before you run it. The path is the same in both lanes.

    sed -n '1,6p' .claude/agents/spec-auditor.md

    name is what you type to call it, description says when it applies, and tools: Read, Glob, Grep holds no edit tool, which makes it an auditor rather than a second implementer. model: opus names its model instead of inheriting the session's.

  2. Run /clear. It is not what isolates the auditor: a subagent starts with a fresh, isolated context window and does not see your conversation, the skills you invoked, or the files Claude already read. What reaches it is a delegation message composed by your session; /clear empties that session, so the message comes from your prompt.

    /clear
  3. Invoke the auditor by name and paste this prompt in full. Let it finish without steering it; a critic you argue with mid-run reports your opinion back to you.

    @"spec-auditor (agent)" Audit the working tree against specs/daily-summary.md. For every finding give four things: the criterion number it violates or the token NO-CRITERION, the file and line, the evidence you read, and a one-line fix. Do not edit any file and do not run the tests.
  4. Write your own list into ../work/e2.md before you open anything else: one line per finding, each starting with CRITERION n or NO-CRITERION.

  5. Open the first reveal and score against the six reference findings: how many you hold, and how many you hold that are not on the list. A finding with no file and line is invented, and costs as much as a missed one: ten minutes of routing. Auditing your own Exercise 1 run? The six are the reference tree's defects, so count one as missed only if step 7 turns its test red in your tree.

  6. Route every real finding and write its destination beside it in ../work/e2.md. Fixing code is ring one, which step 8 hands back to the runner; routing is ring two, with Lesson 3's three destinations: a new or sharpened criterion in specs/daily-summary.md, where most NO-CRITERION findings belong; a line in CLAUDE.md for a fact that outlives this change, paid for by removing one of equal weight, or a .claude/rules/<name>.md with paths: globs; or the bin, with a sentence saying why.

    A line routed to CLAUDE.md needs one more edit before the re-run, or the second audit reports your own line: the spec lists CLAUDE.md out of bounds, and its Definition of done names every file the diff may touch. In specs/daily-summary.md, add this line at the end of the out-of-bounds list, and add CLAUDE.md beside run-log.template.md in the Definition of done:

    - Except one line in `CLAUDE.md`, routed there by hand before this run. It is not the runner's change and not a stop.
  7. Copy in the hidden acceptance suite and run it. It was written against the spec and the two criteria the second reveal sharpens, never against an implementation, so it goes red on every reference defect your tree carries.

    Swift lane
    cp -R ../day-2-loops-and-your-environment/starter/acceptance/change-b/swift/. Tests/LedgerKitTests/ && swift test

    With the reference result in place (the catch-up block), the summary line carries the count:

    	 Executed 38 tests, with 1 test skipped and 6 failures (0 unexpected) in … seconds

    Each failure line names its test, gives the value the run produced and the value expected, and carries the criterion it belongs to, such as criterion 1: LK-006 (-13999, declined) must not be counted.

    Kotlin lane
    cp -R ../day-2-loops-and-your-environment/starter/acceptance/change-b/kotlin/. src/test/kotlin/ && ./gradlew test

    With the reference result in place (the catch-up block), Gradle prints the count, then the verdict:

    91 tests completed, 6 failed, 1 skipped
    …
    BUILD FAILED in …

    Each failure message opens with the criterion it belongs to and closes with the expected and actual values: criterion 1: LK-006 (-13999, declined) must not be counted ==> expected: <0> but was: <-13999>.

    Your own run's tree fails once per reference defect it shares, so its counts differ. Match each message to a row in ../work/e2.md; one you cannot match is the interesting one.

  8. Record the first run's iteration count and save your two revised files and the runner skill where the reset cannot reach them. The e2- names leave Day 1's CLAUDE.md and daily-summary.md in ../work/ untouched:

    grep -c '^| iteration ' run-log.md >> ../work/e2.md && cp CLAUDE.md ../work/e2-CLAUDE.md && cp specs/daily-summary.md ../work/e2-daily-summary.md && cp .claude/skills/run-spec/SKILL.md ../work/e2-SKILL.md

    Quit Claude Code, run the reset at the bottom of this page, cd .., and paste the setup block again (it re-records the same count). The reset replaced the folder your second tab is in; run cd ../day2-work there before you use it. The fresh copy has no implementation and no run-log.md, so the second run counts from zero. Put your files back, with the log format the runner uses:

    cp ../work/e2-CLAUDE.md CLAUDE.md && cp ../work/e2-daily-summary.md specs/daily-summary.md && cp ../work/e2-SKILL.md .claude/skills/run-spec/SKILL.md && cp ../day-2-loops-and-your-environment/starter/run-log.template.md . && ls run-log.template.md

    Your routed line now makes CLAUDE.md differ from the shipped lane, so from here on it shows in both the runner's budget diff and the spec's Definition-of-done diff, and its changed lines count toward the budget. That one is expected, and step 6 wrote it into the spec; any other unlisted file is not.

    Start Claude Code with claude, then:

    /context

    Confirm the revised CLAUDE.md loaded; a run against instructions that never entered the window measures nothing. Then:

    /run-spec specs/daily-summary.md

    When it exits, count the second run's passes and run the acceptance suite against the tree the sharpened spec produced:

    Swift lane
    grep -c '^| iteration ' run-log.md >> ../work/e2.md && cp -R ../day-2-loops-and-your-environment/starter/acceptance/change-b/swift/. Tests/LedgerKitTests/ && swift test

    Green is the Executed … tests summary line carrying 0 failures; the count depends on how many tests your run wrote.

    Kotlin lane
    grep -c '^| iteration ' run-log.md >> ../work/e2.md && cp -R ../day-2-loops-and-your-environment/starter/acceptance/change-b/kotlin/. src/test/kotlin/ && ./gradlew test

    Green is the verdict line BUILD SUCCESSFUL in ….

    A failure here names an acceptance test, and the acceptance test names the finding you binned. Two counts are now in ../work/e2.md, both against the same cap.

  9. Run /clear and paste the step 3 prompt again against the tree the second run left. Treat the answer as a reading, not a verdict: 0 findings. is what the auditor prints when it has nothing, and anything else must name a file and a line.

  10. Write one sentence in ../work/e2.md saying what you measured, with the words n equals one in it: two counts from one spec in one lane is an anecdote, not a trend.

If you see this instead

Swift lane

The acceptance suite does not compile: error: cannot find type 'CurrencyTotal' in scope. The copy holds no Change B implementation. Paste the catch-up block and run step 7 again.

Kotlin lane

The acceptance suite does not compile: unresolved references, among them ChangeBAcceptanceTest.kt:29:55 Unresolved reference 'CurrencyTotal'., then BUILD FAILED. The copy holds no Change B implementation. Paste the catch-up block and run step 7 again.

grep: run-log.md: No such file or directory after the second run. The second tab is still in the copy the reset deleted, so the rest of the command never ran. Run cd ../day2-work there and paste it again.

The auditor edits a file, or runs the tests. Then it is not the auditor: check that .claude/agents/spec-auditor.md is where setup put it, that its tools: line survived, and that you invoked it by name. For a clean tree, run the reset, then the setup and catch-up blocks; the reset spares ../work/, not your own Exercise 1 implementation.

Nine findings, seven of them NO-CRITERION. The spec is too quiet to audit against. Route the two or three that name a real decision.

Self-check

Run this, from inside day2-work

Swift lane
before=$(awk '{print $1}' ../work/e2-swift.txt) && now=$(wc -l < CLAUDE.md) && if [ "$now" -le "$before" ]; then echo "NET-ZERO OK: $now of $before"; else echo "NET-ZERO FAILED: $now of $before"; fi
Kotlin lane
before=$(awk '{print $1}' ../work/e2-kotlin.txt) && now=$(wc -l < CLAUDE.md) && if [ "$now" -le "$before" ]; then echo "NET-ZERO OK: $now of $before"; else echo "NET-ZERO FAILED: $now of $before"; fi

It prints NET-ZERO OK or NET-ZERO FAILED. A CLAUDE.md that grew fails even when every test is green: the added line is charged to every turn.

Done when: the check prints NET-ZERO OK, the acceptance suite is green against the tree the second run left, and ../work/e2.md holds your findings with a destination beside each, both iteration counts, and one sentence containing n equals one.

Reference answers

Open these after your own list exists. The scoring table is also in the repository at starter/answers/reference-findings.md.

The six reference findings
#TagWhat it isWhere it shows
1CRITERION 1A declined amount is counted: every row that is not pending is added into postedMinor2026-03-03, USD postedMinor is −13999 (LK-006, declined)
2CRITERION 2The pending figure is the day's, copied into every currency2026-03-05, BHD pendingMinor is −9630 (LK-016, a GBP row)
3CRITERION 3Days come back oldest firstbyDay returns 2026-03-02 first
4NO-CRITERIONThe order of rows inside a day: the spec never said, the lane's own test did, and the run deleted that test and reversed the rows2026-03-04 comes back LK-013 first
5NO-CRITERIONWhich currencies earn a totals entry was never specified, so declined-only currencies get one2026-03-05 has an EUR entry from LK-017, declined
6CRITERION 4Totals come back in arrival order, not currency-code order2026-03-06 totals read USD, GBP, EUR

Four tagged, two NO-CRITERION. Catching the four and neither of the two is the usual result, and it means the auditor is comparing code to criteria and stopping there; the question it is missing is what the spec did not say that the code had to decide anyway. Fewer than four means it probably could not read what it needed: check that you gave it the spec path and that its tools: line lets it reach the files. More than six, and each extra one needs a file and a line before you route it.

A lost-coverage finding is real, not invented: the deleted test that kept rows inside a day oldest first, the grid's four test names missing from the rewritten summary tests, or checks that compare sets where a criterion asks for an order. That is how findings 3, 4 and 6 stayed green, so count it with those rows.

Where each one goes

Findings 1, 2, 3 and 6 are ring one: fix the code. Each violates a criterion that is already numbered, already binary and already has a test name beside it in the spec's criterion-to-command grid. Nothing about the spec is wrong; the run was. Writing a new rule for a criterion that already exists is how instruction files get long without getting better.

Finding 4 routes to the spec, sharpening criterion 3. The wording the hidden suite was written against: the day order flips, the rows inside a day do not — transactions within a section stay oldest first. Against the spec it is NO-CRITERION: nothing there said which way rows inside a day run. Against the lane it is lost coverage: the shipped summary tests asserted oldest first, and the run deleted that test when it rewrote the file, which is why the reversal stayed green. The sharpened criterion is what stops the next run deleting it for free.

Swift lane

The deleted test is testTransactionsInsideASectionAreOrderedOldestFirst in Tests/LedgerKitTests/LedgerSummaryTests.swift.

Kotlin lane

The deleted test is a section is ordered by postedAt within the day in src/test/kotlin/ledgerkit/LedgerSummaryTest.kt.

Finding 5 routes to the spec, sharpening criterion 4. Add the clause the criterion was missing: no entry for a currency with no posted and no pending row that day. This is the one most people bin as a detail, and it is the one that puts an empty EUR line on a customer's statement. If you put the clause under criterion 1 instead, because the stray entry came from declined rows, that routing is just as right: what matters is that the clause lands in the spec.

Finding 1 also earns the day's one line in CLAUDE.md. The criterion covers this change; the domain rule underneath it — a declined transaction counts toward no total and still appears in the list — is what the next money change will meet again, on a spec nobody has written yet. That is the test for a standing line: not whether it is true, but whether it outlives the spec that produced it.

The reference CLAUDE.md revision, paid for

One line in, two lines out, in both lanes. The sentence that goes in is the same one; what pays for it is whatever that lane's file was already spending those two lines on. The same revision is in the repository at starter/reveal/claude-md-diff.md.

Swift lane

In, under ## Money, where the other domain rules already are:

A declined transaction is still listed and counted in no total; pending money is reported separately from settled money.

Out, from ## Engineering principles, items 9 and 11:

9. Leave the campsite cleaner than you found it.
11. Premature optimization is the root of all evil.

This file carries no statuses line to sharpen, so the new rule is paid for by two principles that name no specific mistake.

Kotlin lane

Out, from ## Domain rules, the two-line statuses bullet:

- Statuses are `pending`, `posted` and `declined`. A declined transaction is
  still a transaction and is still shown to the customer.

In, one line in its place:

- Statuses are `pending`, `posted` and `declined`. A declined transaction is still listed and counted in no total; pending money is reported separately from settled money.

The bullet that comes out is true, and it is the half of the fact the code does not get wrong. The half it gets wrong is the money.

Two lines out and one in, so wc -l CLAUDE.md goes down by one, which is why net-zero is written as same or lower rather than as exactly equal. The arithmetic is not the test, though: ask of each line what specific mistake its removal would cause. The added line names one you watched happen this morning, in a run that was green when it happened.

The alternative the reference run considered and rejected: put the same rule in .claude/rules/money.md with paths: ["**/Money*", "**/LedgerSummary*"] in its frontmatter, so it loads when Claude reads a matching file, not on every tool use. Rejected because the rule is one sentence long and the files it would be scoped to are the ones this module mostly edits, which buys a file and saves nothing.

Reset

Quit Claude Code, then run this from inside day2-work. It deletes the copy, makes it again from the shipped lane, and leaves you inside it. It uses no git and leaves sandbox/ and ../work/ alone.

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 block (from the course root) before you continue.

Sources