A late change, then the card you hand your team

Day 3: Choose your adventure · Exercise 3 of 3 · 25 min

By the end you will have pushed one late requirement through your framework's update path rather than around it, watched the new late-change tests go red then green, and filled an eight-question comparison card your team can read without taking this course.

This continues where Exercise 2 finished in day3-work. Starting here? Do your track's install-and-init from Exercise 1, then Exercise 2 through its self-check (its checkpoint saves time).

Setup

Quit Claude Code and paste this from the course root (cd .. first if you are in the copy); later commands run inside day3-work, answers in ../work/.

Swift lane
mkdir -p work && { [ -d day3-work ] || cp -R sandbox/swift day3-work; } && cd day3-work && cp ../day-3-choose-your-adventure/starter/late-change.md . && cp ../day-3-choose-your-adventure/starter/acceptance/ChangeCLateChangeTests.swift Tests/LedgerKitTests/ && ls late-change.md Tests/LedgerKitTests/ChangeCLateChangeTests.swift

It lists both files.

Kotlin lane
mkdir -p work && { [ -d day3-work ] || cp -R sandbox/kotlin day3-work; } && cd day3-work && cp ../day-3-choose-your-adventure/starter/late-change.md . && cp ../day-3-choose-your-adventure/starter/acceptance/ChangeCLateChangeTest.kt src/test/kotlin/ && ls late-change.md src/test/kotlin/ChangeCLateChangeTest.kt

It lists both files.

Start claude there, with a second tab for shell commands. Your framework can read this test file; step 3's red, before any code changes, proves it bites. Do not run the suite before then.

Steps

  1. Read late-change.md. Before touching the framework, add one sentence to ../work/map.md predicting which existing artifacts must change.

  2. Start the update with your tab's step 2 under Your track. Each prompt stops before code.

  3. Run the suite at that stop. At least one new test fails, and nothing outside the late-change class does. A build applying the newest rule everywhere, as BMAD's checkpoint does, fails both. One that only handles a move to a later day fails just the earlier-day assertion (start day 5 to 2 from 2026-04-03) in testStartDayMovesOnTheEffectiveDate.

    Swift lane
    swift test 2>&1 | grep -A1 "Test Suite 'ChangeCLateChangeTests' [pf]"

    A stand-in build that reads only the first cycle rule printed:

    Test Suite 'ChangeCLateChangeTests' failed at ….
    	 Executed 2 tests, with 6 failures (0 unexpected) in … (…) seconds

    XCTest counts failed XCTAssertEqual calls (the two tests hold seven), so yours may differ. In plain swift test, every error: line names ChangeCLateChangeTests:

    …/Tests/LedgerKitTests/ChangeCLateChangeTests.swift:45: error: -[LedgerKitTests.ChangeCLateChangeTests testStartDayMovesOnTheEffectiveDate] : XCTAssertEqual failed: ("2026-04-05") is not equal to ("2026-04-20") - from 2026-04-01 the cycle opens on the 20th
    Kotlin lane
    ./gradlew test

    The same stand-in printed:

    > Task :test FAILED
    ChangeCLateChangeTest > testPeriodsBeforeTheChangeKeepTheOldStartDay() FAILED
    …
    ChangeCLateChangeTest > testStartDayMovesOnTheEffectiveDate() FAILED
    …
    … tests completed, 2 failed, 1 skipped
    …
    BUILD FAILED in …

    Each test's FAILED line names ChangeCLateChangeTest; > Task :test FAILED is Gradle's task line.

  4. Let the framework implement, with your tab's step 4. Do not hand-edit source.

  5. Run the same check, then Exercise 2's bounds check: the known-issue run and a diff against the shipped lane.

    Swift lane
    swift test 2>&1 | grep -A1 "Test Suite 'ChangeCLateChangeTests' [pf]"
    LEDGER_RUN_KNOWN_ISSUES=1 swift test
    diff -ruN -x .build -x build -x .gradle -x .kotlin -x .claude -x specs -x run-log.md ../sandbox/swift . | grep '^diff ' | grep -v -e ' \./openspec/' -e ' \./docs/' -e ' \./_bmad'

    With the reference in place, the first:

    Test Suite 'ChangeCLateChangeTests' passed at ….
    	 Executed 2 tests, with 0 failures (0 unexpected) in … (…) seconds

    The second, one error: line naming testLEDGER7_JPYRendersWithTwoDecimalPlaces:

    Test Suite 'All tests' failed at ….
    	 Executed … tests, with 1 failure (0 unexpected) in … (…) seconds

    The third:

    diff -ruN … ../sandbox/swift/Sources/LedgerKit/StatementPeriods.swift ./Sources/LedgerKit/StatementPeriods.swift
    diff -ruN … ../sandbox/swift/Tests/LedgerKitTests/ChangeCAcceptanceTests.swift ./Tests/LedgerKitTests/ChangeCAcceptanceTests.swift
    diff -ruN … ../sandbox/swift/Tests/LedgerKitTests/ChangeCLateChangeTests.swift ./Tests/LedgerKitTests/ChangeCLateChangeTests.swift
    diff -ruN … ../sandbox/swift/Tests/LedgerKitTests/Fixtures/cycle-config.tsv ./Tests/LedgerKitTests/Fixtures/cycle-config.tsv
    diff -ruN … ../sandbox/swift/change-c-brief.md ./change-c-brief.md
    diff -ruN … ../sandbox/swift/late-change.md ./late-change.md
    Kotlin lane
    ./gradlew test
    LEDGER_RUN_KNOWN_ISSUES=1 ./gradlew test
    diff -ruN -x .build -x build -x .gradle -x .kotlin -x .claude -x specs -x run-log.md ../sandbox/kotlin . | grep '^diff ' | grep -v -e ' \./openspec/' -e ' \./docs/' -e ' \./_bmad'

    With the reference in place: BUILD SUCCESSFUL in …, then:

    KnownIssueTest > LEDGER-7 the JPY row renders with decimal places it should not have() FAILED
    …
    … tests completed, 1 failed
    …
    BUILD FAILED in …

    The third:

    diff -ruN … ../sandbox/kotlin/change-c-brief.md ./change-c-brief.md
    diff -ruN … ../sandbox/kotlin/late-change.md ./late-change.md
    diff -ruN … ../sandbox/kotlin/src/main/kotlin/ledgerkit/StatementPeriods.kt ./src/main/kotlin/ledgerkit/StatementPeriods.kt
    diff -ruN … ../sandbox/kotlin/src/test/kotlin/ChangeCAcceptanceTest.kt ./src/test/kotlin/ChangeCAcceptanceTest.kt
    diff -ruN … ../sandbox/kotlin/src/test/kotlin/ChangeCLateChangeTest.kt ./src/test/kotlin/ChangeCLateChangeTest.kt
    diff -ruN … ../sandbox/kotlin/src/test/resources/cycle-config.tsv ./src/test/resources/cycle-config.tsv

    Every diff line is under your lane's source or test folders, or is the brief or late-change.md; the StatementPeriods line names your framework's implementation file, your framework's own test file may add a line beside it, and on the LID tab ./CLAUDE.md appears.

  6. Record what became of the requirement you shipped, from the files your tab's step 6 names. Under your prediction in ../work/map.md, write one line: restated, edited in place, or left beside a second copy, and where. It is question six.

  7. Copy the blank card into ../work/; the guard skips one you have started.

    { [ -f ../work/comparison-card.md ] || cp ../day-3-choose-your-adventure/starter/comparison-card.md ../work/; } && ls ../work/comparison-card.md

    Under each of its eight headings, write one bullet starting - , at least a sentence long, from what you observed today, not from this course's pages. Questions two to five restate your map's first four answers; six is step 6's line. For eight, name the failure from Lesson 4, today's date, and the signal Lessons 1 and 2 asked you to pick, with the date you will count it again. Copy the signal from the ## Signal (Lessons 1 and 2) line of ../work/map.md; if that section holds no line of yours (or only the template's placeholder), replace it with one first. A signal names what you count, how often, and when you count next, for example: tickets where someone asked what done meant, counted weekly for two weeks, next count on the date two weeks from today.

  8. Read your card out loud once. An answer you cannot say without "it depends" is one you have not observed: look at the file again.

  9. Compare your card with the reference card below, which is another track's card on purpose.

Your track

OpenSpec: start the update (step 2)

Exercise 2 did not archive its change, so archive it first, from the terminal rather than the chat: the archive is a file move plus a spec sync, and the command line does it in one run with no agent turn to wait for. The first line prints the change's name (add-statement-periods if you took the checkpoint). --yes answers the questions the archive can stop on: whether to sync the delta specs into openspec/specs/, and, if a task is still unchecked (for example one that needs git, which cannot run in day3-work), whether to continue anyway. With every task complete the archive prints Task status: ✓ Complete and --yes just skips the sync question.

openspec list
openspec archive <change-name> --yes
openspec list
ls openspec/specs

The archive moves the change into changes/archive/ under a date-prefixed name; /opsx:archive in the chat does the same with the questions asked one at a time. Then look at what the last two lines printed.

On the build machine, openspec list with nothing in flight printed No active changes found. A ## MODIFIED Requirements delta needs its requirement in openspec/specs/. Then propose:

/opsx:propose The statement cycle start day can change on a given date. The full requirement is in late-change.md in this project folder — read that file. This modifies a requirement that has already shipped and been archived, so express it as a MODIFIED delta against the existing requirement rather than as a new one. Do not write implementation code in this step.

OpenSpec: implement (step 4)

/opsx:apply

If it asks what to work on:

Work the task list for the late-change proposal. Implement only what its delta spec describes. Stay inside Sources/LedgerKit/ and Tests/LedgerKitTests/ on the Swift lane, or src/main/kotlin/ and src/test/kotlin/ on the Kotlin lane, and stop when the task list is complete.

OpenSpec: where to look (step 6)

grep -rn '^## ' openspec/changes/*/specs

The new delta's heading is the record; this exercise stops before archiving it. OpenSpec 1.13.1's sync, which /opsx:archive runs, updates a MODIFIED requirement in place in openspec/specs/, keeping unmentioned scenarios; an ADDED one under a new name lands beside the old.

LID: start the update (step 2)

Start the change as a plain message, as Exercise 2 did; /linked-intent-dev:update-lid is for reconciling drift or changing mode. LID's skill walks a change from the high-level design down, stopping after each phase:

The statement cycle start day can change on a given date. The full requirement is in late-change.md in this project folder — read that file. Walk the arrow from the top: check whether the high-level design covers this change, then update the low-level design, then the LK-PERIOD EARS requirements, then the tests, then the code. Do not edit the code first and back-fill the design. Tell me at each step which artifact you changed and which ID it carries. Stop for my review at the end of each phase.

Run step 3 at the stop after the requirements.

LID: implement (step 4)

After reading the changed requirement:

Continue down the arrow: update the tests for the LK-PERIOD requirements you changed first, then the code, and keep each // @spec annotation on the topmost function that owns the behaviour. Do not change any requirement to match code you have already written — if the code cannot satisfy a requirement, stop and tell me which one.

When it ends, list the // @spec annotations:

Swift lane
grep -rn '@spec LK-PERIOD' Sources Tests
Kotlin lane
grep -rn '@spec LK-PERIOD' src

LID: where to look (step 6)

grep -rn 'LK-PERIOD' docs/intent

One ID with changed text is an edit in place; a new ID repeating an old one is a second copy. Status is an [x], [ ] or [D] marker, or the Verified by: line the build run wrote.

BMAD: start the update (step 2)

Setting a shipped spec's frontmatter status back to ready-for-dev is this course's path, not a documented one:

The statement cycle start day can change on a given date. The full requirement is in late-change.md in this project folder — read that file. Update the existing spec rather than creating a second one, set its status back to ready-for-dev when the spec is updated, and stop there so I can review before you build.

Before step 3, check for one spec, status ready-for-dev:

ls _bmad-output/implementation-artifacts
grep -rn '^status:' _bmad-output/implementation-artifacts --include=SPEC.md --include='spec-*.md'

BMAD: implement (step 4)

After reviewing the spec, hand it to /bmad-build: in 6.12.0 its routing sends a ready-for-dev spec to implementation, then review, skipping the phase-4 chain's bmad-sprint-planning and bmad-code-review. Approve the uv command it asks to run; without uv it halts. Rename the folder in the prompt if yours differs:

/bmad-build Build the updated spec in _bmad-output/implementation-artifacts/statement-periods/SPEC.md. Implement only what changed for the late requirement, keep every change inside Sources/LedgerKit/ and Tests/LedgerKitTests/ on the Swift lane, or src/main/kotlin/ and src/test/kotlin/ on the Kotlin lane, and tell me which stories changed status. Do not use git in this folder: wherever your workflow needs a diff, use the output of diff -ruN -x .build -x build -x .gradle -x .kotlin -x _bmad -x _bmad-output -x .claude ../sandbox/swift . (on the Kotlin lane, ../sandbox/kotlin) as the provided diff, and do not commit.

BMAD: where to look (step 6)

Run the same ls and grep. A second folder or a spec-*.md file there is a second copy; one SPEC.md with changed requirement text is an edit in place.

If you see this instead

The late-change file does not compile. Either this copy has no Change C (finish Exercise 2), or your framework named a type differently from the brief; rename through its artifact, never the test file.

Swift lane
…/Tests/LedgerKitTests/ChangeCLateChangeTests.swift:14:34: error: cannot find type 'StatementCycle' in scope
Kotlin lane
e: …/src/test/kotlin/ChangeCLateChangeTest.kt:19:32 Unresolved reference 'StatementCycle'.
Swift lane

Step 3 prints nothing. A build returning no periods for a two-rule cycle crashes; plain swift test shows:

error: Exited with unexpected signal code 5
…
Swift/ContiguousArrayBuffer.swift:692: Fatal error: Index out of range

That counts as red.

Kotlin lane

Step 3 shows an exception. A build returning no periods for a two-rule cycle fails both tests with java.lang.IndexOutOfBoundsException. That counts as red.

Step 3 is already green. Your framework generalized the cycle in Exercise 2, moves to an earlier day included: say so under question six: you never saw these assertions bite.

At step 5 another test is red, or the diff leaves bounds. Send Exercise 2's repair prompt for your tab with the failing output; do not edit the test.

On the OpenSpec tab, openspec/specs/ is empty after the archive. It skipped the sync, so a MODIFIED delta has nothing to modify; OpenSpec's archive text calls that sync-blocked. Propose anyway and write that under question six.

Self-check

Run these, from inside day3-work

First, the two late-change tests.

Swift lane
swift test 2>&1 | grep -A1 "Test Suite 'ChangeCLateChangeTests' [pf]"

Pass is passed with Executed 2 tests, with 0 failures. No output means a compile error or a crash; plain swift test shows which.

Kotlin lane
./gradlew test
grep -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' build/test-results/test/TEST-ledgerkit.ChangeCLateChangeTest.xml

Pass is BUILD SUCCESSFUL in … from the first command and tests="2" skipped="0" failures="0" errors="0" from the second. After BUILD FAILED with e: lines, the report is left over from an earlier run.

Then the bounds.

Swift lane
LEDGER_RUN_KNOWN_ISSUES=1 swift test 2>&1 | grep -e ': error: ' -e "Test Suite 'All tests' [pf]" -A1

Pass is one error: line, naming testLEDGER7_JPYRendersWithTwoDecimalPlaces, and one failure in the summary:

…/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
--
Test Suite 'All tests' failed at ….
	 Executed … tests, with 1 failure (0 unexpected) in … (…) seconds
Kotlin lane
LEDGER_RUN_KNOWN_ISSUES=1 ./gradlew test

Pass is … tests completed, 1 failed, on KnownIssueTest alone.

Then the card.

grep -c '^- .\{40,\}' ../work/comparison-card.md

Pass is 8; the blank card prints 0.

Done when: a late-change test was red at step 3, both were green at step 5 with nothing else red, JPY alone fails the known-issue run, the diff stays in bounds, the card check prints 8, and ../work/map.md holds step 1's prediction and step 6's line.

Reference answers

Open these after your card has eight answers. Unmarked lines are documented, or observed on the build machine on 2026-09-22; "this course's path" marks how this course runs the tool.

Your tab shows a different track's card, not your own: you already have your track's answers from your files, so the useful comparison is with a tool you did not run. Go heading by heading and look for where the two cards differ, then check that each of your answers names a file you opened rather than something the other card could have told you.

You ran OpenSpec: the reference card for BMAD
  1. Installs. npx bmad-method install, needing Node.js 20.12 or later and uv; on the build machine 6.12.0 wrote 235 files (a _bmad/ tree, 29 skills under .claude/skills/, an empty _bmad-output/) and left CLAUDE.md alone.
  2. Specs live in a spec folder under _bmad-output/implementation-artifacts/: SPEC.md, stories.yaml, and stories/<id>-<slug>.md.
  3. Creates a spec: /bmad-build followed by what you want built; on Change C it wrote the spec folder with status ready-for-dev.
  4. Implements: /bmad-build on a ready-for-dev spec goes to implementation, then a review step. The documented phase-4 chain is bmad-sprint-planning, then bmad-build, then bmad-code-review.
  5. After ship the spec's frontmatter status carries its state, one of the six values that drive BMAD's unattended loop.
  6. Late requirement: update the spec, set its status back to ready-for-dev, and hand it to /bmad-build (this course's path, not a documented one). Not run on the build machine.
  7. Would not want: every skill starts by running a uv command, and on the build machine a 6.12.0 session without permission to run shell commands halted before planning. In a locked-down environment it does not start. A judgement built on that observation.
  8. Adopt it to fix Lesson 4's third question: work that starts before anyone has decided scope, sequence or review.
You ran LID: the reference card for OpenSpec
  1. Installs. npm install -g @fission-ai/openspec@latest, needing Node 20.19.0 or later; pinned at 1.13.1. On the build machine openspec init wrote fifteen files: openspec/config.yaml, two .gitkeep placeholders under openspec/, and six commands and six skills under .claude/. It wrote nothing into CLAUDE.md or AGENTS.md.
  2. Specs live in openspec/specs/; changes in flight in openspec/changes/<name>/, which on Change C held proposal.md, design.md, tasks.md, .openspec.yaml and one delta spec per capability.
  3. Creates a spec: /opsx:propose <feature description>.
  4. Implements: /opsx:apply, working the task list.
  5. After ship: /opsx:archive moves the change into changes/archive/ under a date-prefixed name.
  6. Late requirement: the documented path is a second proposal whose delta sits under ## MODIFIED Requirements, then apply and archive; the archive updates the shipped requirement in place. Not run on the build machine.
  7. Would not want: telemetry is on unless each machine turns it off, with openspec config set telemetry.enabled false or OPENSPEC_TELEMETRY=0, which a regulated team has to remember everywhere. A judgement.
  8. Adopt it to fix Lesson 4's second question: a shipping codebase that changes a ticket at a time, where every spec attempt grows into a document about the whole system.
You ran BMAD: the reference card for LID
  1. Installs two Claude Code plugins, /plugin marketplace add jszmajda/lid then /plugin install linked-intent-dev@jszmajda-lid and /plugin install arrow-maintenance@jszmajda-lid, which at commit 831c1959 added about 1,090 tokens to every session on the build machine; the first /arrow-maintenance:map-codebase run there appended a 29-line ## LID section to CLAUDE.md with no marker comments; that run could not load the plugin's own skill body, so your run may write a different block.
  2. Specs live in docs/intent/<segment>/<segment>-specs.md, beside a design file per segment, below a high-level design at docs/high-level-design.md and an overlay index at docs/arrows/index.yaml.
  3. Creates a spec: /arrow-maintenance:map-codebase builds the first tree from existing code; a later change starts from a prompt that describes it (this course's path).
  4. Implements: tests, then code, are the last links of the same cascade, and code carries // @spec with the requirement IDs.
  5. After ship nothing is archived: the requirement stays in the tree, marked [x] implemented, [ ] gap or [D] deferred in LID's documented format; the build's Change C run wrote a Verified by: line instead.
  6. Late requirement: a prompt that walks the arrow from a check of the high-level design down through the low-level design, requirements, tests and code, stopping after each phase (this course's path). Not run on the build machine.
  7. Would not want: it writes into CLAUDE.md without markers, so the section is yours to keep short by hand. A judgement.
  8. Adopt it to fix Lesson 4's first question: nobody can say which requirement a line of code serves.
Where the late change lands, per track

This build did not push the late change through any framework. Change C's planning ran through all three on 2026-09-22; the late change did not. So there is no build-machine observation to compare against, only the path each prompt on this page asks for. Your run is the observation: one machine, one date.

No implementation code is given for any track, and none for your own.

Reset

Quit Claude Code, then from inside day3-work run your lane's line; ../work/ is untouched.

Swift lane
cd .. && rm -rf day3-work && cp -R sandbox/swift day3-work && cd day3-work
Kotlin lane
cd .. && rm -rf day3-work && cp -R sandbox/kotlin day3-work && cd day3-work

Then run your track's install-and-init steps from Exercise 1, then Exercise 2 through its self-check, then cd .. and paste this page's setup block. OpenSpec on your PATH and the LID plugins at user scope survive; BMAD lives in the project, so rerun its installer. Do not reset at the end: Lesson 5 and the wrap-up's third challenge read this day3-work.

Sources