LEDGER-7 in plan mode

Day 1: From chat to spec · Exercise 2 of 4 · 25 min

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.

Swift lane
rm -rf day1-work && cp -R sandbox/swift day1-work
Kotlin lane
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
1Does it name the file it will change?
2Does it name each currency and its minor-unit count?
3Does it name the exact verification command?
4Does it say how the gated test gets run?
5Does it stay out of LedgerSummary and the fixture?
6Does it say what it will not change?

Steps

  1. 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.md under Before now.

    Swift lane
    LEDGER_RUN_KNOWN_ISSUES=1 swift test

    From 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 lane
    LEDGER_RUN_KNOWN_ISSUES=1 ./gradlew test

    From 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 failed line and the BUILD FAILED verdict line. The lane's build.gradle.kts declares LEDGER_RUN_KNOWN_ISSUES as 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 lane
    swift test

    Copy the line beginning Executed 34 tests, with 1 test skipped.

    Kotlin lane
    ./gradlew test

    Copy the BUILD SUCCESSFUL verdict line.

  2. Read LEDGER-7.md in 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.

  3. Start Claude Code in plan mode from inside day1-work, or press Shift+Tab in a running session until the mode line reads plan mode on — the keystroke cycles default, acceptEdits, plan.

    claude --permission-mode plan
  4. 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.
  5. Run the six items against the proposal in order, filling the table in ../work/plan.md with the line that answers each one.

  6. Reject once: answer No, keep planning and send one sentence naming the item number and what is missing. Record it in ../work/plan.md under 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.

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

  8. Approve each edit as it is proposed and let the run finish.

  9. 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.md under After.

    Swift lane
    swift test

    Green, 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 test
    Kotlin lane
    ./gradlew test

    Green 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.xml

    After the fix, step 1's KnownIssueTest failure is gone and the verdict reads BUILD SUCCESSFUL. The grep prints skipped="0" when the gate reached the test and skipped="1" when it did not; BUILD SUCCESSFUL alone cannot tell you which happened. The verdict line and skipped="0" together are your After evidence.

  10. Check that the fix was not a loosened test. The shipped lane one level up is the before picture and day1-work the after, so one recursive diff is the record of the run: -N counts a file the agent created as well as one it edited, and each file it touched opens a section with a line starting diff.

    Swift lane
    diff -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.md
    Kotlin lane
    diff -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.md

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

  11. Once ../work/plan.md holds 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

Swift lane

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.

Kotlin lane

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.

Kotlin lane

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:

Swift lane
diff -u ../sandbox/swift/Tests/LedgerKitTests/KnownIssueTests.swift Tests/LedgerKitTests/KnownIssueTests.swift
Kotlin lane
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:

Swift lane
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.
Kotlin lane
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
CurrencyMinor unitsRenders as
USD2-$84.99
EUR2-€23.50
GBP2-£12.99
JPY0-¥128,000
BHD3-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.

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