Make it critique itself
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.
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
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.
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
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
-
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.mdnameis what you type to call it,descriptionsays when it applies, andtools: Read, Glob, Grepholds no edit tool, which makes it an auditor rather than a second implementer.model: opusnames its model instead of inheriting the session's. -
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;/clearempties that session, so the message comes from your prompt./clear -
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. Write your own list into
../work/e2.mdbefore you open anything else: one line per finding, each starting withCRITERION norNO-CRITERION.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.
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 inspecs/daily-summary.md, where mostNO-CRITERIONfindings belong; a line inCLAUDE.mdfor a fact that outlives this change, paid for by removing one of equal weight, or a.claude/rules/<name>.mdwithpaths:globs; or the bin, with a sentence saying why.A line routed to
CLAUDE.mdneeds one more edit before the re-run, or the second audit reports your own line: the spec listsCLAUDE.mdout of bounds, and its Definition of done names every file the diff may touch. Inspecs/daily-summary.md, add this line at the end of the out-of-bounds list, and addCLAUDE.mdbesiderun-log.template.mdin 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.-
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 lanecp -R ../day-2-loops-and-your-environment/starter/acceptance/change-b/swift/. Tests/LedgerKitTests/ && swift testWith 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 … secondsEach 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 lanecp -R ../day-2-loops-and-your-environment/starter/acceptance/change-b/kotlin/. src/test/kotlin/ && ./gradlew testWith 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. -
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'sCLAUDE.mdanddaily-summary.mdin../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.mdQuit 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; runcd ../day2-workthere before you use it. The fresh copy has no implementation and norun-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.mdYour routed line now makes
CLAUDE.mddiffer 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:/contextConfirm the revised
CLAUDE.mdloaded; a run against instructions that never entered the window measures nothing. Then:/run-spec specs/daily-summary.mdWhen it exits, count the second run's passes and run the acceptance suite against the tree the sharpened spec produced:
Swift lanegrep -c '^| iteration ' run-log.md >> ../work/e2.md && cp -R ../day-2-loops-and-your-environment/starter/acceptance/change-b/swift/. Tests/LedgerKitTests/ && swift testGreen is the
Executed … testssummary line carrying0 failures; the count depends on how many tests your run wrote.Kotlin lanegrep -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 testGreen 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. Run
/clearand 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.Write one sentence in
../work/e2.mdsaying 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
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.
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
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
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
| # | Tag | What it is | Where it shows |
|---|---|---|---|
| 1 | CRITERION 1 | A declined amount is counted: every row
that is not pending is added into postedMinor | 2026-03-03, USD
postedMinor is −13999 (LK-006, declined) |
| 2 | CRITERION 2 | The pending figure is the day's, copied into every currency | 2026-03-05, BHD pendingMinor is −9630
(LK-016, a GBP row) |
| 3 | CRITERION 3 | Days come back oldest first | byDay returns 2026-03-02 first |
| 4 | NO-CRITERION | The 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 rows | 2026-03-04 comes back LK-013 first |
| 5 | NO-CRITERION | Which currencies earn a totals entry was never specified, so declined-only currencies get one | 2026-03-05 has an EUR entry from LK-017, declined |
| 6 | CRITERION 4 | Totals come back in arrival order, not currency-code order | 2026-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.
The deleted test is testTransactionsInsideASectionAreOrderedOldestFirst in
Tests/LedgerKitTests/LedgerSummaryTests.swift.
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.
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.
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.
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 block (from the course root) before you continue.
Sources
cc-core-25,cc-core-26,cc-core-27,cc-core-28— the subagent file, itsmodel:andtools:fields, and explicit invocation. From Subagents, accessed 2026-09-22.cc-core-38— a subagent's fresh, isolated context and the delegation message. From Subagents, accessed 2026-09-23.cc-core-19,cc-core-18—/clearand/context. From Slash commands, accessed 2026-09-22.context-eng-03—/contextconfirmsCLAUDE.mdloaded. From Best practices, accessed 2026-09-22.cc-core-15,cc-core-40— path-scoped rule files, and that they load when Claude reads a matching file, not on every tool use. From Memory, accessed 2026-09-22 and 2026-09-23.cc-core-35,cc-core-36—CLAUDE.mdloads at startup, before your first message, and reloads after compaction. From Explore the context window, accessed 2026-09-22.cc-core-32— the version checked against, from the Claude Code changelog, 2026-09-22.- Terminal output recorded on 2026-09-23 in fresh copies made by this page's setup and catch-up blocks.