2026-10-15T23:54:36Z. Step: s1. Unit: noun-verb map. Tests added: 0. Cycles: 0. Sha: 23297a0. Agent: mapped-path:s1. Tokens: 387027. Seconds: 90.877.
Actions: Built MAP.md from README.md and docs/SPEC.md with 15 nouns. Split Board, Game and Player, which had over 4 verbs. Added parents House(Pit) and Store(Pit) and verb1(Noun2.verb3) references. Added tools/check_map.py and confirmed it flags an over-4 noun, a dangling parent, a dangling reference and a call cycle on a scratch map. The final map has 0 problems. No ./run.sh call because the step names none.
2026-10-15T23:55:32Z. Step: s2. Unit: map call edges. Tests added: 0. Cycles: 0. Sha: 0b81ee0. Agent: mapped-path:s2. Tokens: 318251. Seconds: 50.58.
Actions: Read each verb1(Noun2.verb3) reference as an edge Noun.verb1 -> Noun2.verb3, with inherited verbs resolved to Pit. Extracted call_edges in tools/check_map.py and added --edges to print them. Added a Call edges section to MAP.md with 64 edges from 24 callers and 19 leaf verbs. Map check still reports 0 problems. No ./run.sh call because the step names none.
2026-10-16T00:11:43Z. Step: L3-8 iteration 1. Unit: Quit before any move shows the anti-clockwise 6-4 board, the P1 prompt and Game over. Tests added: 25. Cycles: 15. Sha: e3a3c87. Agent: mapped-path:L3-8#1. Tokens: 4565339. Seconds: 978.58.
Actions: Wrote UNITS.md with 11 features. Acceptance test AcceptanceTest.testQuitBeforeAnyMoveAnticlockwise plays examples quit.txt and failed first. Route leaves first: Variant.fromFlags, Player.label, Pit.seedCount/isEmpty, Board.create/house/store, Board.isSideEmpty, BoardView.drawBorder, drawStore, drawSide, draw, Message.prompt/gameOver, Command.read/isQuit, Turn.take, Game.isOver, Game.quit, Game.play. Each verb got a failing unit test then the smallest change, then ./run.sh test and ./run.sh lint. Trivial getters shared a cycle with their verb to fit the 15-cycle limit. Re-routed in MAP.md first: Board.create takes counts and a new Kalah.play entry builds Variant, Board and Game, both to pass PMD CouplingBetweenObjects 5. Wiring Kalah.play closed the acceptance test without its own unit cycle. Added test helper ScriptedIO because the Write tool strips the trailing space spec prompts need. Acceptance and ExampleAnticlockwiseTest.testQuit pass. Lint clean. Five example tests stay red for later features. Conflict with standing guidance: one sed edit to BoardTest.java where Edit was required.
2026-10-16T00:14:02Z. Step: L3-8 iteration 2. Unit: Clockwise board layout with stores and labels swapped. Tests added: 4. Cycles: 3. Sha: 260c048. Agent: mapped-path:L3-8#2. Tokens: 1394665. Seconds: 137.902.
Actions: Acceptance test AcceptanceTest.testQuitBeforeAnyMoveClockwise plays Kalah.play(clockwise 6-4) with ScriptedIO and asserts the board lines from useempty.txt, because no clockwise quit spec exists. It failed first on the anti-clockwise layout. Route leaves first: Variant.isClockwise (existing, tested), BoardView.drawSide, BoardView.draw. Added edges BoardView.draw -> Variant.isClockwise and Player.label to MAP.md first. Map check 0 problems, 67 edges. Cycles: clockwise P2 houses count up, clockwise P1 houses count down (refactored into private isLabelFirst), draw swaps stores and labels through private drawRow. ./run.sh test and ./run.sh lint after each verb. Acceptance passes and lint is clean. Three clockwise example tests now fail at line 11 after the first move instead of line 2. Five example tests stay red for later features. Map diff: only the two new BoardView.draw edges.
2026-10-16T00:15:01Z. Step: L3-8 iteration 3. Unit: 7 houses with 5 seeds board. Tests added: 1. Cycles: 0. Sha: f3257c0. Agent: mapped-path:L3-8#3. Tokens: 376246. Seconds: 48.17.
Actions: Acceptance test AcceptanceTest.testQuitBeforeAnyMoveSevenFive plays Kalah.play(anti-clockwise 7-5) with ScriptedIO and asserts the board lines from the 7-5 simple_start.txt layout. It passed on first run, so step 3 saw no red. The quit assertion moved into a shared private assertQuitShows. Route leaves first: Variant.fromFlags (7-5 branch already unit tested), Board.create(7, 5) (already unit tested), BoardView.drawBorder, drawSide, draw, which all take the house count from Variant. No verb needed a change and the route lacked no verb, so 0 red-green cycles and no unit tests. ./run.sh test: acceptance passes, the same five example tests stay red for later features. ./run.sh lint clean. Map diff: none.
2026-10-16T00:20:11Z. Step: L3-8 iteration 4. Unit: Sow a move on the player's own side and pass the turn. Tests added: 11. Cycles: 10. Sha: f75c088. Agent: mapped-path:L3-8#4. Tokens: 3035994. Seconds: 317.437.
Actions: Acceptance test AcceptanceTest.testSowOnOwnSidePassesTurn plays Kalah.play(anti-clockwise 6-4) with inputs 1 then q and asserts P1 houses 2 to 5 at 5 seeds and the P2 prompt. It failed first. Route leaves first: Pit.takeAll, Pit.addSeed, Command.houseNumber, Player.next, Ring.nextPit, Move.sow, Move.nextPlayer, Move.apply, Turn.take, Game.play. Re-routed MAP.md first: Ring.nextPit calls Variant.houseCount in place of Variant.isClockwise because the specs sow house n to n+1 in both directions and only the drawing mirrors. Map check 0 problems, still 67 edges. Each verb got a failing unit test then the smallest change, then ./run.sh test and ./run.sh lint. Ring holds only houses so far and Move.nextPlayer always passes the turn; stores and extra moves wait for later features. Turn.take returns the next player or null on quit. Game takes its Turn and Kalah.play builds Ring, Move and Turn inline because declared locals pushed Kalah to PMD coupling 6. Acceptance passes, lint clean. ExampleClockwiseTest.testSimpleStart75 now passes. Four example tests stay red for later features. Map diff: only the Ring.nextPit edge.
2026-10-16T00:24:14Z. Step: L3-8 iteration 5. Unit: Sow into the player's own store, skip the opponent's store and wrap around. Tests added: 5. Cycles: 3. Sha: e5d0366. Agent: mapped-path:L3-8#5. Tokens: 2044031. Seconds: 240.993.
Actions: Acceptance test AcceptanceTest.testSowIntoOwnStoreSkipOpponentStoreAndWrap replays the first nine moves of exactlapemptycapture.txt, then q. The prefix has no capture and no extra move. Moves 3 to 8 sow into the own store and P1's ninth move skips P2's store and wraps to P1 house 1. It failed first. Route leaves first: Store.owner, Ring.nextPit (own store, then skip and wrap), Move.sow. Each verb got a failing unit test then the smallest change, then ./run.sh test and ./run.sh lint. Move.sow only passes the sowing player to Ring.nextPit, so it got no own unit cycle. A first instanceof check broke PMD InstanceOfUse and CastUse and a Store list then pushed Ring to coupling 6. Re-routed MAP.md first: Ring takes a house count from Kalah.play and drops its Variant.houseCount edges. Map check 0 problems, 65 edges. Acceptance passes and lint is clean. Four example tests stay red for later features. Map diff: the two Variant.houseCount edges left Ring.
2026-10-16T00:25:51Z. Step: L3-8 iteration 6. Unit: Last seed in the player's own store gives another move. Tests added: 3. Cycles: 1. Sha: 74399f4. Agent: mapped-path:L3-8#6. Tokens: 865298. Seconds: 91.31.
Actions: Acceptance test AcceptanceTest.testLastSeedInOwnStoreGivesAnotherMove replays full_game2.txt to line 31, then q. P2's moves 3 and 2 end in P2's store, so P2 is asked twice in a row. It failed first. The prefix replay moved into a shared private assertSpecPrefixThenQuit. Route leaves first: Board.store, Player.next (both existing and tested), Move.nextPlayer, Move.apply. Re-routed MAP.md first: Move.nextPlayer calls Board.store in place of Store.owner and compares the last pit with the mover's store, because a type check on Pit breaks PMD InstanceOfUse. Map check 0 problems, still 65 edges. One red-green cycle covered Move.nextPlayer and Move.apply together, because the new nextPlayer(player, last) signature forces apply to pass the sown pit in the same change. MoveTest got two new tests and its old apply test moved to house 1, since house 3 now ends in the store. ./run.sh test and ./run.sh lint after the verb. Acceptance passes and lint is clean. Four example tests stay red for later features, now at lines 111, 90, 22 and 26. Map diff: Move.nextPlayer -> Store.owner became Move.nextPlayer -> Board.store.
2026-10-16T00:27:43Z. Step: L3-8 iteration 7. Unit: Choosing an empty house reports it and asks again. Tests added: 4. Cycles: 3. Sha: 4ffd6df. Agent: mapped-path:L3-8#7. Tokens: 968742. Seconds: 106.242.
Actions: Acceptance test AcceptanceTest.testEmptyHouseIsReportedAndAskedAgain plays useempty.txt (clockwise 6-4) through ExampleSpec.play. It failed first at the line after P1 picks the empty house 1. Route leaves first: Pit.isEmpty, Board.house (both existing and tested), Message.emptyHouse, Move.isLegal, Turn.take. The map already held every verb and edge, so no re-route. Each verb got a failing unit test then the smallest change, then ./run.sh test and ./run.sh lint. Turn.take prints the message and returns the same player, so Game.play redraws the board and asks again. Acceptance passes and lint is clean. ExampleClockwiseTest.testUseEmpty now passes and testFullGameTie moved from line 26 to 203. Three example tests stay red for later features. Map diff: none.
2026-10-16T00:31:10Z. Step: L3-8 iteration 8. Unit: Capture when the last seed lands in an empty own house opposite a non-empty house. Tests added: 10. Cycles: 6. Sha: a1fd096. Agent: mapped-path:L3-8#8. Tokens: 2412141. Seconds: 212.014.
Actions: Acceptance test AcceptanceTest.testCaptureFromTheOppositeHouse replays full_game2.txt to line 94, then q. P2's move 2 ends in the empty house 4 facing P1's house 3 with 7 seeds, so P2's store goes from 6 to 14. It failed first. Route leaves first: Ring.oppositeHouse, Ring.isOwnHouse, Capture.applies, Capture.collect, Capture.tryCapture, Move.apply. Re-routed MAP.md first: House.owner left the map and Ring gained -isOwnHouse, because reading an owner from the last Pit would need a cast that breaks PMD. Ring.oppositeHouse and Ring.isOwnHouse read the pit order and became leaves. Capture.collect gained the Ring.oppositeHouse edge. Map check 0 problems, still 65 edges, now 23 calling verbs and 21 leaves. Each verb got a failing unit test then the smallest change, then ./run.sh test and ./run.sh lint. Move builds its own Capture so Kalah.play stays within PMD coupling. Acceptance passes and lint is clean. ExampleAnticlockwiseTest.testExactLapEmptyCapture now passes. testFullGame2 and testFullGameTie stay red at the game end. Map diff: House.owner removed, Ring.isOwnHouse added, Ring.oppositeHouse -> Board.house removed, Capture.applies -> House.owner became Capture.applies -> Ring.isOwnHouse, Capture.collect -> Ring.oppositeHouse added.
2026-10-16T00:32:28Z. Step: L3-8 iteration 9. Unit: No capture when the opposite house is empty, including an exact lap. Tests added: 1. Cycles: 0. Sha: efe6caa. Agent: mapped-path:L3-8#9. Tokens: 453639. Seconds: 67.521.
Actions: Acceptance test AcceptanceTest.testNoCaptureWhenTheOppositeHouseIsEmpty replays full_game2.txt to line 166, then q. P1 move 1 ends in the empty house 2 facing the empty P2 house 5, so no seeds move to a store and P2 moves next. Conflict with step 3: the test passed on first run and was never red, because iteration 8 built the empty-opposite check into Capture.applies with CaptureTest.testDoesNotApplyWhenTheOppositeHouseIsEmpty. The exact lap half is already pinned by ExampleAnticlockwiseTest.testExactLapEmptyCapture, which passes. Route leaves first: Pit.isEmpty, Ring.oppositeHouse, Ring.isOwnHouse, Capture.applies, Capture.tryCapture, Move.apply. Every verb already had a unit test covering this path, so no unit red-green cycle ran and no production code changed. ./run.sh test and ./run.sh lint: acceptance passes, lint clean, ExampleAnticlockwiseTest.testFullGame2 and ExampleClockwiseTest.testFullGameTie stay red at the game end. Map diff: none.
2026-10-16T00:36:19Z. Step: L3-8 iteration 10. Unit: Game ends when the player to move has no seeds, with scores and the winner. Tests added: 6. Cycles: 5. Sha: 7d881d6. Agent: mapped-path:L3-8#10. Tokens: 1715620. Seconds: 228.851.
Actions: Acceptance test AcceptanceTest.testGameEndsWithScoresAndWinner plays full_game2.txt through ExampleSpec.play. It failed first at line 217, where P1 has no seeds after P2's last move. Route leaves first: Pit.seedCount, Board.house, Board.store (existing), Score.total, Score.winner, Message.result, Game.finish, Game.play. Re-routed MAP.md first twice. Game.finish gained a BoardView.draw edge because no turn draws the board after the last move. Then Game hit PMD CouplingBetweenObjects 6 with a Score field, so Board.isSideEmpty moved to Score.isSideEmpty and Game holds a Score in place of a Board. Its two BoardTest tests moved to ScoreTest. Map check 0 problems, 66 edges, 23 calling verbs and 21 leaves. Each verb got a failing unit test then the smallest change, then ./run.sh test and ./run.sh lint. Score.winner returns null on a tie and Message.result only prints a winner line, so the tie stays for the next feature. Acceptance passes and lint is clean. ExampleAnticlockwiseTest.testFullGame2 now passes. ExampleClockwiseTest.testFullGameTie stays red with a null winner at the result line. Map diff: Board.isSideEmpty removed, Score.isSideEmpty added, Game.isOver -> Board.isSideEmpty became Game.isOver -> Score.isSideEmpty, Game.finish -> BoardView.draw added.
2026-10-16T00:37:37Z. Step: L3-8 iteration 11. Unit: A tie result when the scores are equal. Tests added: 2. Cycles: 1. Sha: 608f2bf. Agent: mapped-path:L3-8#11. Tokens: 614240. Seconds: 597.695.
Actions: Acceptance test AcceptanceTest.testTieWhenTheScoresAreEqual plays full_game_tie.txt (clockwise 6-4) through ExampleSpec.play. It failed first with a NullPointerException in Message.result on the null winner. Route leaves first: Pit.seedCount, Board.house, Board.store, Score.total, Score.winner (existing, ScoreTest.testNoWinnerOnATie already pins null on a tie), Message.result. The map already held every verb and edge, so no re-route. One red-green cycle: MessageTest.testResultReportsATie failed, then Message.result prints "A tie!" when the winner is null. ./run.sh test and ./run.sh lint after the verb. Lint flagged line length on the new ternary, so it was split while green. All 16 test classes pass, including ExampleClockwiseTest.testFullGameTie, and lint is clean. Map diff: none.