LEDGER-7 in plan mode
Take a seeded bug through plan mode, reject one plan on a defect you can name, run the approved plan to green, and finish holding a test that failed before the change and passes after it.
Setup
You continue in day1-work, the copy Exercise 1 made. Paste this block from the
course root (cd .. first if you are in the copy): it puts the ticket beside the code
and the blank checklist at ../work/plan.md, which no reset touches, and leaves you
inside day1-work, where every later command on this page runs. The shipped
sandbox/ is never edited.
mkdir -p work && cp day-1-from-chat-to-spec/starter/tickets/LEDGER-7.md day1-work/ && cp day-1-from-chat-to-spec/starter/plan-checklist.md work/plan.md && cd day1-work && ls LEDGER-7.md ../work/plan.md
It prints the two paths it made; ../work/plan.md is your deliverable. Start
Claude Code from inside day1-work, since a session starts in the directory you
launch it from.
Budget five minutes for each agent run; the 25 minutes above assume three of them back to back.
Starting from a clean copy
If Exercise 1 left the copy in a state you would rather not plan against, make it again from the course root, then paste the setup block above. A copy made this way is identical to the shipped lane, which is what step 10 measures against.
rm -rf day1-work && cp -R sandbox/swift day1-work
rm -rf day1-work && cp -R sandbox/kotlin day1-work
The six items
Step 5 runs these against the proposal; ../work/plan.md holds them as a table.
Each answer is yes or no; if you have to infer it, it is no.
| # | Question |
|---|---|
| 1 | Does it name the file it will change? |
| 2 | Does it name each currency and its minor-unit count? |
| 3 | Does it name the exact verification command? |
| 4 | Does it say how the gated test gets run? |
| 5 | Does it stay out of LedgerSummary and the fixture? |
| 6 | Does it say what it will not change? |
Steps
-
Run your lane's gated command and confirm exactly one test fails, on the JPY case. That run is half your evidence and is gone once the bug is fixed, so copy its summary line into
../work/plan.mdunder Before now.Swift laneLEDGER_RUN_KNOWN_ISSUES=1 swift testFrom a run of this lane on 2026-09-22, path and timings shortened, error line wrapped:
…/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 34 tests, with 1 failure (0 unexpected) in … seconds …Copy the line beginning
Executed 34 tests.Kotlin laneLEDGER_RUN_KNOWN_ISSUES=1 ./gradlew testFrom a run of this lane on 2026-09-22, stack frames and timing shortened, assertion wrapped:
> Task :test FAILED KnownIssueTest > LEDGER-7 the JPY row renders with decimal places it should not have() FAILED org.opentest4j.AssertionFailedError: LEDGER-7: LK-014 is -128000 minor units of JPY. The fixture expects "-¥128,000" and TransactionFormatter.amountText produced "-¥1,280.00". Money.decimalPlaces returns 2 for JPY; JPY has no minor unit. ==> expected: <-¥128,000> but was: <-¥1,280.00> … 85 tests completed, 1 failed BUILD FAILED in … …Copy the
85 tests completed, 1 failedline and theBUILD FAILEDverdict line. The lane'sbuild.gradle.ktsdeclaresLEDGER_RUN_KNOWN_ISSUESas a test-task input, so the tests re-run when only the gate changes.Then run the plain command, the same one without the variable. It should be green now, and step 9 checks it is still green after the fix, so copy its result into the
Plain test command, before:slot under Before.Swift laneswift testCopy the line beginning
Executed 34 tests, with 1 test skipped.Kotlin lane./gradlew testCopy the
BUILD SUCCESSFULverdict line. Read
LEDGER-7.mdin the copy: a normal bug report describing a customer's screenshot, with no acceptance criteria. The two complaints at the bottom are not part of this fix.-
Start Claude Code in plan mode from inside
day1-work, or press Shift+Tab in a running session until the mode line readsplan mode on— the keystroke cyclesdefault,acceptEdits,plan.claude --permission-mode plan -
Paste this prompt as written and let the agent read the code and propose a plan. Nothing in the copy changes while the proposal is on screen.
Read LEDGER-7.md and the LedgerKit source, then propose a plan to fix it. Bounds: the fix belongs in the money formatting path - Money and TransactionFormatter and their tests. Do not change LedgerSummary, LedgerFilter, or the fixtures file. Do not change the assertions in the known-issue test - it is already correct, it is only gated off. The "When it is fixed" cleanup in KNOWN-ISSUES.md is not part of this fix: leave the golden fixture test's skip of the JPY row in place. Verification: the plan must name the exact command I will run to prove the fix, including how the gated test gets executed. Stop: if you believe the fix requires touching a file outside the money formatting path, say so in the plan instead of planning around it. Run the six items against the proposal in order, filling the table in
../work/plan.mdwith the line that answers each one.Reject once: answer No, keep planning and send one sentence naming the item number and what is missing. Record it in
../work/plan.mdunder Rejection 1. If it fails several, name the lowest-numbered one that fails. If the first proposal passes all six, reject it on the weakest; most first plans name a test command but not the output that counts as success. Then answer only the item you rejected on, against the second proposal.When the second proposal passes that item, press Ctrl+G and edit the plan yourself: add the verification command if it is missing, delete any step touching a file outside the bounds, including one that removes the golden fixture test's JPY skip. Approve with Yes, manually approve edits; the other answer applies edits without asking.
Approve each edit as it is proposed and let the run finish.
-
Verify twice. The plain command, green at step 1, must stay green: it guards against fixing one currency by breaking the other four. Only the gated command proves the bug is gone; your lane's After evidence goes into
../work/plan.mdunder After.Swift laneswift testGreen, before and after. Its summary line:
Executed 34 tests, with 1 test skipped and 0 failures (0 unexpected) in … seconds …After the fix it reads 34 tests, or 35 if the plan added one. Then the gated run again. After the fix, step 1's failure block is gone and the summary line reports 0 failures, nothing skipped: that line is your After evidence.
LEDGER_RUN_KNOWN_ISSUES=1 swift testKotlin lane./gradlew testGreen before and after, with the verdict
BUILD SUCCESSFUL. Then the gated run again, and its test report:LEDGER_RUN_KNOWN_ISSUES=1 ./gradlew test grep -o 'skipped="[0-9]*"' build/test-results/test/TEST-ledgerkit.KnownIssueTest.xmlAfter the fix, step 1's
KnownIssueTestfailure is gone and the verdict readsBUILD SUCCESSFUL. The grep printsskipped="0"when the gate reached the test andskipped="1"when it did not;BUILD SUCCESSFULalone cannot tell you which happened. The verdict line andskipped="0"together are your After evidence. Check that the fix was not a loosened test. The shipped lane one level up is the before picture and
day1-workthe after, so one recursive diff is the record of the run:-Ncounts a file the agent created as well as one it edited, and each file it touched opens a section with a line startingdiff.Swift lanediff -ruN -x .build ../sandbox/swift . | grep '^diff 'Whatever your run did, one line is the ticket the setup copied in:
diff -ruN -x .build ../sandbox/swift/LEDGER-7.md ./LEDGER-7.mdKotlin lanediff -ruN -x build -x .gradle -x .kotlin ../sandbox/kotlin . | grep '^diff 'Whatever your run did, one line is the ticket the setup copied in:
diff -ruN -x build -x .gradle -x .kotlin ../sandbox/kotlin/LEDGER-7.md ./LEDGER-7.mdEvery other line names a file your run touched, the lane's copy and yours. The money formatting path and its tests are what the prompt allowed. A line naming
LedgerSummary,LedgerFilter, the fixture or the known-issue test is the finding.Once
../work/plan.mdholds your After evidence and the self-check below passes, run the one-line Reset command for your lane and leave the copy reset for Exercise 3.
If you see this instead
The gated run reports 1 test skipped and 0 failures. The
variable did not reach the test; it has to sit on the same command line, in front of
swift test.
The gated run reports BUILD SUCCESSFUL before the fix. Same
cause: the variable has to be set on the command line that starts Gradle.
A file changes while the proposal is still on screen. The session was not in plan mode when you pasted the prompt, or started with bypass permissions available, the exception Lesson 3 named. Reset (foot of this page), check the mode line, paste the prompt again.
The plain command goes red after your run. The plan was wrong and the diff is the evidence. Take the same reset, then reject the next plan on item 2.
Self-check
One command decides it, run twice
Same pair in both lanes: the gated command failed on the JPY case at step 1 and passes now, while the plain command is green at both ends.
Here, passing means BUILD SUCCESSFUL and step 9's grep printing
skipped="0"; a run the gate never reached is BUILD SUCCESSFUL too.
Then check that the known-issue test did not move:
diff -u ../sandbox/swift/Tests/LedgerKitTests/KnownIssueTests.swift Tests/LedgerKitTests/KnownIssueTests.swift
diff -u ../sandbox/kotlin/src/test/kotlin/ledgerkit/KnownIssueTest.kt src/test/kotlin/ledgerkit/KnownIssueTest.kt
It prints nothing while the test is as the lane shipped it. A hunk means the assertion was edited: the bug argued away, not fixed.
Done when: the self-check passes and
../work/plan.md holds both Before lines from step 1, a rejection sentence naming an item
number, the plan you approved, and step 9's After evidence.
Reference answers
Open these after your run; the fix is not in them. The same three sections are
in the repository, reached from
starter/answers/e2-plan-checklist.md,
which points at plan-checklist.md beside it.
What a passing answer looks like for each of the six items
1. The file it will change. Yes looks like
Sources/LedgerKit/Money.swift, or
src/main/kotlin/ledgerkit/Money.kt, written out once as a path. No looks like
“update the money formatting logic”: true, and it does not tell you whether one file
or three is about to move. A plan naming two files fails this item only if it does not say what
each one gets.
2. Each currency and its minor-unit count. Yes looks like the five codes with their counts in the plan text, before any code is written. No looks like “give JPY zero decimal places”, which fixes the ticket and leaves you no way to tell whether the other four are about to change by accident. This is the item most first plans fail, because the ticket mentions one currency and the plan mirrors the ticket.
3. The exact verification command. Yes looks like swift test or
./gradlew test spelled out, plus what counts as success. No looks like “run
the tests to confirm”, which a sentence in the final message can satisfy.
4. How the gated test gets run. Yes looks like the gated command named as a step of its own. No looks like silence, and silence is the expensive answer here: the suite is green before the fix and green after it, so a plan that only runs the plain command has verified nothing about the bug.
5. Staying out of LedgerSummary and the fixture. Yes looks like
no step touching either, and ideally a line saying so. No looks like a step that edits the
fixture or “updates the expected value”: the fixture already holds the right answer,
so a plan proposing to change it has decided the bug is in the test.
6. What it will not change. Yes looks like one line naming the neighbours it is leaving alone. Most plans have no such line unless the prompt asks for one, which is why the prompt asks for one. It is the cheapest signal you get about whether the boundary was understood or is going to be discovered halfway through.
The two rejection sentences the reference run sent
Rejection 1, against a plan that failed item 4:
Item 4: the plan verifies with swift test only, which is green before and
after the fix. Name the gated command as a separate step and say what its
output is before the change and after it.
Item 4: the plan verifies with ./gradlew test only, which is green before
and after the fix. Name the gated command as a separate step and say what
its output is before the change and after it.
Rejection 2, against a plan that passed all six, so it was rejected on the weakest item:
Item 2: you list JPY at zero but not the other four. Put USD, EUR, GBP and
BHD in the plan with their counts so I can see at review time that the
table is complete rather than patched.
The second one is the skill this exercise is teaching. That plan was not wrong; it was reviewable in principle rather than reviewable in ten seconds, and saying so cost one sentence. A rejection that names no item number and no missing thing is not a rejection, it is a mood, and “this needs more detail” sends the agent to guess which detail.
The minor-unit table the fix needs
| Currency | Minor units | Renders as |
|---|---|---|
| USD | 2 | -$84.99 |
| EUR | 2 | -€23.50 |
| GBP | 2 | -£12.99 |
| JPY | 0 | -¥128,000 |
| BHD | 3 | -BD12.500 |
These are the five currencies the fixture uses, and its expectedAmountText
column already holds every string in the right-hand column. BHD surprises people twice: three
decimal places is correct, and the ticket's second complaint is about how it reads, not about
whether it is right.
Reset
One command, the sandbox reset every Day 1 exercise means: delete the copy, remake it from the shipped lane, step back in.
cd .. && rm -rf day1-work && cp -R sandbox/swift day1-work && cd day1-work
cd .. && rm -rf day1-work && cp -R sandbox/kotlin day1-work && cd day1-work
It never reaches ../work or writes to sandbox/, so your checklist
outlives it. It does remove the LEDGER-7.md the setup copied in; this brings it
back:
cp ../day-1-from-chat-to-spec/starter/tickets/LEDGER-7.md .
Not the setup block: it would overwrite ../work/plan.md, now holding your
evidence, with the blank checklist. Run the reset once more when you finish, and leave the copy
reset for Exercise 3.
Sources
cc-core-01,cc-core-03,cc-core-37— Shift+Tab cyclesdefault,acceptEditsandplan; the mode line readsplan mode on;claude --permission-mode planstarts in plan mode.cc-core-04— Ctrl+G edits the proposed plan.cc-core-05— the approval answers, including Yes, manually approve edits and No, keep planning.cc-core-33,cc-core-34— plan mode researches and proposes without editing your source, except in an interactive terminal session with bypass permissions available.- All seven from Permission modes, accessed 2026-09-22.
cc-core-41— the directory you launch Claude Code in is the session's primary working directory, from Permissions, accessed 2026-09-23.cc-core-32— the version checked against, from the Claude Code changelog, 2026-09-22.- Terminal output recorded from both lanes of this repository on 2026-09-22; the Kotlin test-report grep on 2026-09-23.