Same task, two prompts

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

Take one real task two ways — read it the way you would ask chat, then run it as the four-part assignment from Lessons 1 and 2 — and finish with two captured diffs and a five-row table showing what the extra ninety seconds of prompt bought you.

Setup

Paste one block from the course root, the folder holding this course's index.html. It copies your lane to day1-work, leaves your shell inside the copy, and makes work/ for your answers. You never edit sandbox/ itself.

Swift lane
mkdir -p work
cp -R sandbox/swift day1-work && cd day1-work

Prompt B also ships as ../day-1-from-chat-to-spec/starter/prompts/e1-prompt-b.txt.

Kotlin lane
mkdir -p work
cp -R sandbox/kotlin day1-work && cd day1-work

Prompt B also ships as ../day-1-from-chat-to-spec/starter/prompts/e1-prompt-b-kotlin.txt.

Every later command on this page, and every path inside the prompts, is written from inside day1-work. Starter files are one level up, at ../day-1-from-chat-to-spec/starter/…, and answers go in ../work/. If day1-work is already there, step into it and run the reset at the bottom of this page instead.

Start Claude Code in the copy with claude: a session starts in the directory you launch it from. Prompt A is the same in both lanes and ships as ../day-1-from-chat-to-spec/starter/prompts/e1-prompt-a.txt.

Keep a second terminal tab open in day1-work: the session holds the tab you start it in, so every other shell command on this page goes in the second.

Budget five minutes for the one agent run; the 20 minutes above assume Prompt A is read, not run, and step 2 says why.

One thing about both prompts is deliberate: the premise is wrong. There is no Cafe Lumiere in the fixtures — the café rows are Café Müller — and LedgerFilter's merchant match already ignores case and diacritics. Vague bug reports arrive like this, and part of what you measure is whether the agent reads the code first. The table measures scope and evidence, not whether the bug was real.

Steps

  1. Confirm the starting state: your lane's test command passes.

    Swift lane
    swift test

    A green run on this machine on 2026-09-22 printed the suite's pass line and its count line:

    Test Suite 'All tests' passed at 2026-09-22 19:41:49.214.
    	 Executed 34 tests, with 1 test skipped and 0 failures (0 unexpected) in 0.017 (0.021) seconds

    The run also prints Test run with 0 tests in 0 suites passed after 0.001 seconds. The suite is XCTest, so swift-testing has nothing of its own to run. Timestamps and durations vary; the counts do not. The one skipped test is deliberate, and you meet it in Exercise 2.

    Kotlin lane
    ./gradlew test

    A first run in a fresh copy on this machine on 2026-09-22 printed:

    BUILD SUCCESSFUL in 1s
    5 actionable tasks: 5 executed

    A Gradle configuration-cache hint prints with it; BUILD SUCCESSFUL is the line that decides. The duration varies, and a re-run reports the tasks as up to date.

    Then confirm the copy still matches the lane it came from. It prints nothing when the two trees are identical, and you run it again after every reset:

    Swift lane
    diff -ruN -x .build ../sandbox/swift .
    Kotlin lane
    diff -ruN -x build -x .gradle -x .kotlin ../sandbox/kotlin .
  2. Read Prompt A, but do not paste it. It is the request as you would type it into chat, and it names no file:

    The merchant search in this project doesn't work very well - searching for cafe doesn't find Cafe Lumiere. Can you fix that?

    Run A is a non-event by design. With the premise wrong and nothing to check against, the agent reads the code, finds the match already ignores case and diacritics, and changes nothing — the third outcome in the second reveal below. Five minutes watching that buys you nothing the reveal does not, so the untouched copy stands in for it as the reference run A.

  3. Capture run A from the untouched copy. The shipped lane one level up is the before picture and day1-work the after, so one recursive diff is the record of a run; here it writes an empty file, which is what a run that changed nothing records.

    Swift lane
    diff -ruN -x .build ../sandbox/swift . > ../work/run-a.diff
    Kotlin lane
    diff -ruN -x build -x .gradle -x .kotlin ../sandbox/kotlin . > ../work/run-a.diff

    -N makes a file the agent created show up as a whole-file addition, and created files are the ones people forget to count. -x drops the build products. Each changed file opens with a header line starting diff, so counting those lines counts the files touched:

    grep -c '^diff ' ../work/run-a.diff
  4. In Claude Code, paste Prompt B exactly as written and approve edits as they come. Do not correct it, take up its follow-up suggestions, or paste failures back to it: you are measuring what the prompt produced, not what you rescued. If the session already holds an earlier conversation, run /clear first so run B inherits nothing; Lesson 6 explains why that is a budget decision and not only tidiness. Prompt B is the same request as Prompt A with the four questions answered.

    Swift lane
    Goal: LedgerFilter's merchant match should ignore case and diacritics, so the query "cafe" matches the merchant "Cafe Lumiere" and the query "CAFE" matches it too.
    
    Bounds: you may change Sources/LedgerKit/LedgerFilter.swift and Tests/LedgerKitTests/LedgerFilterTests.swift and nothing else. Do not change Sources/LedgerKit/Money.swift, Sources/LedgerKit/TransactionFormatter.swift, Sources/LedgerKit/LedgerSummary.swift, the fixtures file, or CLAUDE.md.
    
    Verification: run swift test and show me the Executed line right under Test Suite 'All tests' passed (or failed). Add at least one test for the diacritic case and one for the case-insensitive case, using merchants that already exist in the fixtures.
    
    Stop: if the change needs a file outside those two paths, stop and tell me which file and why, without editing it. If the same test fails twice in the same way, stop and show me the failure.
    Kotlin lane
    Goal: LedgerFilter's merchant match should ignore case and diacritics, so the query "cafe" matches the merchant "Cafe Lumiere" and the query "CAFE" matches it too.
    
    Bounds: you may change src/main/kotlin/ledgerkit/LedgerFilter.kt and src/test/kotlin/ledgerkit/LedgerFilterTest.kt and nothing else. Do not change src/main/kotlin/ledgerkit/Money.kt, src/main/kotlin/ledgerkit/TransactionFormatter.kt, src/main/kotlin/ledgerkit/LedgerSummary.kt, the fixtures file, or CLAUDE.md.
    
    Verification: run ./gradlew test and show me the BUILD line. Add at least one test for the diacritic case and one for the case-insensitive case, using merchants that already exist in the fixtures.
    
    Stop: if the change needs a file outside those two paths, stop and tell me which file and why, without editing it. If the same test fails twice in the same way, stop and show me the failure.
  5. Capture run B the same way, writing to ../work/run-b.diff, then run your lane's test command again and record the result.

  6. Fill the five rows below in ../work/e1-table.md. Rows one, two and five come off the captures, not your memory of the runs; row three is the command you ran in steps 1 and 5, step 1's run standing for run A because its copy is untouched; row four is the transcript, and no for run A, which had no session. Row five is insertions plus deletions, with each file's two header lines left out:

    grep -vE '^(--- |\+\+\+ )' ../work/run-a.diff | grep -cE '^[+-]'
    Copy this into ../work/e1-table.md and fill both columns.
    RowRun ARun B
    Files touched——
    Files touched outside the two in-bounds paths——
    Tests pass or fail, in your own run——
    Did the agent run the verification command itself——
    Lines changed——

    Both captures spell the two in-bounds paths as the prompt named them, behind the prefix each diff header adds: the sandbox path (../sandbox/swift/ or ../sandbox/kotlin/) on the before side and ./ on the after side. Match on the path's end, not the whole string:

    Swift lane

    Sources/LedgerKit/LedgerFilter.swift and Tests/LedgerKitTests/LedgerFilterTests.swift.

    Kotlin lane

    src/main/kotlin/ledgerkit/LedgerFilter.kt and src/test/kotlin/ledgerkit/LedgerFilterTest.kt.

  7. Open the reveal, check how each row is decided, then name in one sentence which prompt part did the most work.

If you see something else instead

Likely wrong turns

Swift lane

A line beginning error: instead of the count line means the package did not compile, a different problem from a failing test: the agent's last edit is syntactically broken, and the capture is where to look.

Kotlin lane

BUILD FAILED with a FAILED line naming a test class means a test went red, and the HTML report path printed with it is worth opening on the first failure.

Self-check

Two commands decide it

The first shows both captures exist and how big each is. The second prints every file run B touched that the prompt did not allow, and nothing when run B stayed in bounds; it works in either lane, because both lanes' in-bounds files carry LedgerFilter in their names.

wc -l ../work/run-a.diff ../work/run-b.diff
grep '^diff ' ../work/run-b.diff | grep -v LedgerFilter

Each path it prints goes straight into row two. Run the same grep against run-a.diff for run A.

Done when: both captures exist, the grep above has run against each, all five rows are filled from them and your own test runs, and the sentence naming the prompt part that did the most work is written down.

Reference answers

How each row is decided, and the two rows people fill in wrong

Rows one, two and five are arithmetic on a file. Files touched is the number of diff header lines in the capture; lines changed is what the insertions-and-deletions grep in step 6 printed; out of bounds is what the self-check grep printed. Tests pass or fail is your own run, after the agent stopped, not its report of one.

Row four is the one people get wrong. The mistake is answering yes because the final message says the tests pass. A summary that reads "I ran the tests and they pass" is the sentence that usually follows a change like this one, and it can be produced without any command having run. The evidence that counts is a tool call in the transcript with output attached to it. If you cannot find the call, the answer is no, whatever the summary said. Prompt B asks for the command and names the output line that carries its result, the Executed line in Swift and the BUILD line in Kotlin, for exactly this reason: a request for the output is a request for the run.

Row two is the other one. The mistake is reading a green suite as in bounds. A vague prompt can touch four files, pass every test, and still be the change you did not want: a helper extracted into Money, a signature widened in the formatter, a tidy in the summary type while it was there. Nothing breaks, because none of those files had a failing test waiting for them. Green says nothing about scope; the capture says everything about scope, and you can read its header lines in one screen.

Reading your pair: the three outcomes, and which one you got

Run B stays in bounds and proves its change. The usual case. Most people name bounds as the part that did the work, because that is the row that visibly changed. It is often verification: an agent given a command to run and an output to show tends to make a change it can prove, and a change it can prove is a smaller change.

Run B also changes nothing. This happens, more often on a small well-named task in a small repository than people expect, and it is not a failed exercise. The four parts are insurance; insurance that did not pay out this time still cost ninety seconds. What you have learned is what a task looks like when it was not needed, which is worth knowing, because the next task will be bigger and you will not be able to tell in advance.

Run A did nothing at all. The reference run A in step 2: zero files, and the prompt's own wording invited it. An agent that declines to change working code is behaving well; a prompt that could not tell you whether the code worked is the finding.

No recorded run B capture ships with this exercise. Two runs of the same prompt differ, and printing one machine's numbers here as the answer would be the mistake row four is about — a claim standing in for a command you ran. Your own run B file is the evidence, and the longer version of this note is in ../day-1-from-chat-to-spec/starter/answers/e1-comparison.md.

Reset

One command, the sandbox reset every Day 1 exercise means: delete the copy, remake it from the shipped lane, step back in. Quit Claude Code before you run it and start it again with cd ../day1-work && claude in the first tab. It never reaches ../work or writes to sandbox/, so your captures outlive it. Run it once more when you finish, and leave the copy reset for Exercise 2, which continues in day1-work.

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

When the day is finished, delete the copy from the course root with rm -rf day1-work. Keep work: Day 2 and Day 3 write there too, and the Day 3 wrap-up removes it when you are done for good.

Sources