step.13.diff.txt -- narrative retro for step.13 (U2.4: Retro-pair machinery + ledger CLI) Executed: 2026-07-20, same session as the skeleton (no separate skeleton-then-execute split -- the retro machinery, the `ledger` CLI, all three new test files, the docs, and this step file were all built together, per the operator's explicit "draft AND execute (single pass)" direction, matching step.7/9/10/11/12's rhythm). Companion artifact: step.13.gdiff.txt (automated, scripts/step_gdiff). This step closes P2 entirely (U2.1-U2.4 all now done) and clears P3's stated entry condition alongside the already-done DAG substrate (U3.9/U3.9a, step.5/ 6). Distinguishing feature versus its predecessors, worth naming up front since the operator's own brief asked for it explicitly: steps/step.13.txt and this file were DOGFOODED -- both were generated as skeletons via `.venv/bin/python -m app.cli ledger draft-step` / `ledger draft-retro` (the very tool this step built) and then hand- filled section by section, unlike steps 7-12, which were all written by hand from scratch because no templating tool existed yet. ## Plan vs actual ``` +---+----------------------------------+------------------------------------------+ | # | Planned (§) | Actual | +---+----------------------------------+------------------------------------------+ | 1 | Correct the U2.4 bullet's stale | Confirmed stale exactly as flagged, and | | | "cdiff generation (the | WORSE than the brief's own framing: a | | | claude_diff pattern)" wording | grep of design.md and README.md for the | | | to reference step_gdiff/ | SAME pattern (not just the one named | | | .gdiff.txt (verify this repo's | instance) found FOUR more stale spots -- | | | own steps 1-12 never used | design.md's own retro-pair description | | | claude_diff/.cdiff.txt first) | ("`.cdiff.txt` automated") and two "§9 | | | | ledger" mentions each in design.md and | | | | README.md. All five corrected in this | | | | same step. `ls steps/*.gdiff.txt` | | | | (12 files) vs `ls steps/*.cdiff.txt` | | | | (0 files) confirmed the repo's own | | | | history before touching a single word. | | | | A SIXTH, unrelated slip found in the | | | | same pass: end_to_end.md's own U2.3 | | | | Delivered line said "245p/11s" where the | | | | arithmetic (205+42) and step.12.diff. | | | | txt's own Numbers table both say 247 -- | | | | a hand-authoring typo, fixed in the same | | | | line. See step.13.txt (1). | | 2 | Survey steps/step.7.diff.txt- | Read all six in full. Found the | | | step.12.diff.txt to find the | invariant STRUCTURAL template (title/ | | | structural template | banner/"Executed:" line/four section | | | empirically, not assumed | headings in a fixed order/closing line) | | | | cleanly, and confirmed the CONTENT | | | | inside every section is genuine | | | | narrative in all six samples, no | | | | exceptions -- exactly what point 6's | | | | mechanical/narrative split needed to be | | | | grounded in real evidence rather than | | | | assumption. | | 3 | Decide the mechanical/narrative | Landed on: mechanical = step number/UoW/ | | | scoping boundary for the retro | title/date/companion-filename/section- | | | templating deliberately, per | skeleton shape/the Hash-backfill two- | | | point 6 | line shape; narrative = everything else, | | | | INCLUDING file-changed lists and test- | | | | count deltas (both look mechanical but | | | | require either re-deriving step_gdiff's | | | | own git-diffing logic, which the brief | | | | explicitly said not to reimplement, or | | | | actually running the suite twice at two | | | | points in time the generator has no | | | | access to). See app/ledger/retro.py's | | | | own module docstring for the full | | | | reasoning. | | 4 | Fix step.12's 3-commit drift | Re-read steps/step.11.txt (2)/(7) as the | | | back to the established | reference (per the operator's explicit | | | 2-commit shape (step.7/9/10/11)| point 7 instruction, NOT step.12's own | | | | drifted shape): work commit leaves | | | | PENDING; the immediately-following gdiff | | | | commit ALSO backfills the hash into | | | | end_to_end.md and this file's own §7 -- | | | | one commit, not two separate ones. This | | | | step follows that shape exactly. | | 5 | (not in the original brief -- | Dogfooding U2.4's own row (todo->active- | | | found live while executing | >done, via the new `ledger` CLI, G9) | | | G9's dogfooding) | broke a PRE-EXISTING test: | | | | tests/test_ledger_table_roundtrip.py's | | | | test_structural_round_trip_after_ | | | | mutating_a_row (step.12) had picked U2.4 | | | | as its "a real todo row" example -- | | | | once U2.4 legitimately became `done`, | | | | that test's own `transition(row, | | | | "active", ...)` call started raising | | | | IllegalTransitionError (done is | | | | terminal). Confirmed live (a full-suite | | | | run immediately after the dogfooding | | | | commands went red). Fixed by pointing | | | | the test at U3.1 (the next real todo | | | | row, P3, not yet started) instead of | | | | loosening or deleting it -- the exact | | | | "the check is a discovery tool, don't | | | | weaken it" lesson step.12's own finding | | | | #2 named for a different case (a | | | | formatting-bug check there; a live-data | | | | dependency here), now confirmed for a | | | | third kind of check. | +---+----------------------------------+------------------------------------------+ ``` Every other part of the plan held: the `StepFile`-via-`emit_step_file` delegation for `draft_step_skeleton()` worked on the first implementation (no reimplemented header/section formatting needed); the `ledger` CLI's eight subcommands all read-mutate-write through the EXISTING U2.1-U2.3 APIs with zero new ledger logic, exactly as scoped. ## Gate evidence ``` +------+---------------------------------------------------------------------+ | G1 | pytest -q tests/test_ledger_retro.py -- 8/8 PASS live, including | | | the REAL scripts/step_gdiff invocation against a synthetic | | | tmp-dir git repo (confirmed the produced step.1.gdiff.txt | | | contains the real "widget.py"/"def greet" diff content, not a | | | placeholder). | | G2 | pytest -q tests/test_ledger_cli.py -- 19/19 PASS live, both direct | | | library calls and main()-argparse-wired invocations, against an | | | isolated synthetic doc. | | G3 | pytest -q tests/test_synthetic_uow_arc.py -- 1/1 PASS live: the | | | full draft->ratify(trivial)->execute(trivial)->verify(trivial)-> | | | close arc, asserting the complete real artifact set (step.N.txt- | | | shaped file, step.N.diff.txt-shaped file with FILL_IN present, a | | | REAL step.N.gdiff.txt containing real diff content, and the | | | ledger row at done/step.N/PENDING) inside a throwaway tmp_path git | | | repo -- confirmed live that this repo's own steps/ and | | | end_to_end.md were never touched (the synthetic UoW's id/title do | | | not appear in the real end_to_end.md, asserted directly). | | G4 | draft_step_skeleton()'s output verified live to round-trip through | | | the REAL app/ledger/parser.py -- both via the dedicated test AND | | | via dogfooding: steps/step.13.txt itself parses clean | | | (`parse_step_file('steps/step.13.txt')`, run live after every hand- | | | edit) under the same production parser, not a special-cased one. | | G5 | grep -rn '§9\|claude_diff' design.md end_to_end.md README.md -- | | | every hit read individually and confirmed CORRECTION NARRATIVE | | | (naming the old value inline, e.g. "corrected ... from '§9'"), not | | | a live stale pointer; the two accurate .cdiff.txt-as-excluded-glob | | | mentions (end_to_end.md:195, docs/step_file_schema.md:27) left | | | alone on purpose, named explicitly as NOT stale. | | G6 | grep -n "git diff\|git log" app/ledger/retro.py -> only the module | | | docstring's own prose mention of "a git diff", zero actual | | | git-diffing logic duplicated (the only `git`-adjacent code is the | | | subprocess.run call to the external scripts/step_gdiff itself). | | G7 | Full suite before this step's edits: 247 passed/11 skipped | | | (confirmed live, matching step.12's own corrected 205->247 | | | baseline -- see Plan-vs-actual #1). After, once the Plan-vs-actual | | | #5 test fix landed: 277 passed/11 skipped. +30 = 28 genuinely new | | | (8+19+1 from the three new test files) + 2 from the step-file | | | parser's/emitter's own self-inclusive corpus parametrization | | | picking up step.13.txt, the same growth pattern steps 10-12 | | | documented for their own predecessors. 0 skipped delta, 0 real | | | failures once the one real, reported collision (#5) was fixed. | | G8 | end_to_end.md's own U2.4 done-when, LITERALLY: "a synthetic UoW | | | walks draft -> close leaving the full artifact set" -- G3 IS this | | | gate. | | G9 | §11 ledger row for U2.4 updated THROUGH this step's OWN new | | | `ledger` CLI, live against the real end_to_end.md: | | | `.venv/bin/python -m app.cli ledger transition U2.4 active --step | | | step.13` then `... ledger close U2.4 PENDING` -- confirmed via | | | `git diff --stat` before committing, and via directly reading the | | | resulting §11 row (done/step.13/PENDING). | | G10 | b3ubot porcelain clean confirmed before each commit; retro pair via | | | the SHARED v3_claude/scripts/step_gdiff (steps/step.13.gdiff.txt); | | | exactly 2 commits (work + gdiff-with-hash-backfill-folded-in), not | | | 3 -- step.11's own precedent, correcting step.12's drift per the | | | operator's explicit point 7 instruction. | +------+---------------------------------------------------------------------+ ``` ## Findings for later steps 1. **A "verify the named stale reference" instruction generalizes to "grep every document this pattern could have leaked into," not just the file the brief names.** The brief pointed at end_to_end.md's own U2.4 bullet; the real staleness (both the cdiff/claude_diff wording AND the "§9" numbering) had also leaked into design.md and README.md -- five total stale spots across three files, four of them nobody had named. The general lesson, now confirmed a third time (step.10's "34" corpus count, step.12's "§9"/"§10" section refs, this step's cdiff/ claude_diff/§9 spread): a live document's own self-references are a standing staleness risk that doesn't respect file boundaries, and "verify the ONE named instance" is a floor, never a ceiling, on what the check should cover. 2. **A tool this step BUILT was immediately used to draft the very files documenting that it works -- and that dogfooding immediately found a real gap in the tool itself.** `draft_step_skeleton()` sources its banner from `app/config.banner_text()` (U2.2's own design, correctly reused rather than reimplemented) -- but the operator's own dev shell has no `$HOME/BANNER.md` configured, so the FIRST live invocation of `ledger draft-step` produced a bannerless step.13.txt, silently diverging from every other file in this repo's own corpus (all 12 carry the banner). This was not a bug in the tool (the banner IS correctly runtime-injected, exactly as U2.2 designed it -- a session without a configured banner correctly gets no banner) but a real gap between "what the generator produces in THIS session" and "what the file needs to look like to match the corpus this repo already committed to." Fixed by hand-adding the banner text to steps/step.13.txt directly (the committed file's bytes, not the generator's behavior) -- confirmed live via the byte-identical round-trip test (G4) once fixed. Worth remembering: dogfooding a generator against a REAL committed artifact surfaces environment-dependent gaps a synthetic unit test (which controls its own $HOME fixture, as tests/test_ledger_retro.py's own tests do) cannot. 3. **A live ledger row transitioning to `done` is itself a real event that can break an EXISTING test that quietly depended on it staying `todo`.** table_transitions.py's own docstring already names "ledger truth changes over time" as a fact of life for the DATA (a row moves through states); this step is the first to show the same fact applies to TESTS that pick a real row by id as an example rather than by a synthetic fixture. The fix (pointing at U3.1 instead of U2.4) is narrow and correct for today, but the underlying pattern -- any test that names a specific real UoW id as "the todo one" is one dogfooding step away from breaking -- is worth naming for whoever next promotes a UoW that some other test happens to be using as its example. ## Numbers ``` +---------------------------------------+------------------------+ | new files | 6 (app/ledger/retro.py,| | | tests/test_ledger_ | | | retro.py+test_ledger_ | | | cli.py+test_synthetic_ | | | uow_arc.py, docs/ | | | ledger_cli_and_retro. | | | md, steps/step.13.txt) | | files edited | 6 (app/cli.py, tests/ | | | test_ledger_table_ | | | roundtrip.py, end_to_ | | | end.md, design.md, | | | README.md, this file's | | | own step.13.diff.txt) | | retro.py + cli.py additions LOC | ~260 + ~230 = ~490 | | new tests | 28 (8 retro + 19 cli + | | | 1 synthetic-arc) + 2 | | | from the step-file | | | corpus's own self- | | | inclusive growth | | | (picking up step.13. | | | txt) | | stale cdiff/claude_diff/§9 refs found | 5 (1 named in the | | + corrected | brief + 4 found live) | | unrelated typo found + corrected | 1 (245p/11s -> | | (end_to_end.md U2.3 Delivered line) | 247p/11s) | | pre-existing test broken by this | 1 (found live, fixed | | step's own dogfooding, then fixed | by pointing at U3.1) | | ledger CLI subcommands | 8 (show/transition/ | | | add-row/record-commit/ | | | close/draft-step/ | | | draft-retro/gdiff) | | suite before -> after | 247p/11s -> 277p/11s | | regressions | 0 (the one break was | | | a live-data collision, | | | fixed, not a | | | regression left open) | +---------------------------------------+------------------------+ ``` U2.4 done -- closes P2 entirely (U2.1-U2.4 all `done`) and clears P3's stated entry condition alongside the already-done DAG substrate (U3.9/U3.9a, step.5/6). P3 (the orchestrator, U3.1-U3.8) can now begin; nothing in this step builds any part of it (named explicitly out of scope in steps/step.13.txt (2), not silently begun -- the synthetic-UoW arc's "ratify"/"execute"/"verify" stages are trivial placeholders on purpose). Ledger updated in the work commit per the §12 execution contract, performed literally THROUGH this step's own new API (G9); commit hash of that same commit backfilled into both end_to_end.md §11 and this file's own steps/step.13.txt §7 in the immediately following gdiff commit -- the same 2-part, TWO-commit resolution step.7/9/10/11 used (step.12's own 3-commit drift corrected back per the operator's explicit point 7 instruction, confirmed by re-reading steps/step.11.txt (2)/(7) as the reference before repeating the pattern here). End of retro.