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.
