step.20.diff.txt -- narrative retro for step.20 (U3.7: RETRO closure -- retro pair + ledger row + hash backfill + the execution DAG FROZEN as the step's evidence graph) Executed: 2026-07-21, same session as the draft (single pass, the step.7/9/10/11/14-19 convention -- closing.py, the CloseError addition, the instance_is_frozen/refuse_if_frozen freeze wall in executing.py + its verifying.py call sites, the `close` CLI subcommand, the 17 test additions, the design.md/end_to_end.md/ orchestrator_state_machine.md edits, and the step file all built together). Companion artifact: step.20.gdiff.txt (automated, scripts/step_gdiff). Per the operator's explicit per-UoW checkpoint rule (HARD), this session STOPS after U3.7 -- U3.8 is not started. ## Plan vs actual ``` +---+----------------------------------+------------------------------------------+ | # | Planned (brief) | Actual | +---+----------------------------------+------------------------------------------+ | 1 | Decide retro drafting V0: PAL- | DETERMINISTIC (the brief's own LEAN, | | | narrated vs deterministic | taken and owned): closure must be | | | evidence-filled | reliable/cheap at the moment evidence | | | | is sealed; every V0 fact already exists | | | | as a machine record (engine history, | | | | node kvs, verifications.jsonl); U2.4's | | | | no-fabricated-reflection rule holds. | | | | The drafted retro fills the corpus | | | | structural template with mechanical | | | | evidence and NAMES itself | | | | MACHINE-DRAFTED; PAL retro = post-M1. | | 2 | Decide the freeze's V0 semantics | frozen_at dag-level kv (design section | | | | 3's WorkflowDag field name verbatim; | | | | substrate round-trip CONFIRMED live | | | | BEFORE committing to it) + the code- | | | | enforced refuse_if_frozen wall in | | | | executing AND verifying (text-sniff | | | | guard, works parser-less; refusals | | | | trail-recorded). NOT chmod bits -- | | | | neither portable policy nor honest | | | | protection. | | 3 | Decide git-commit-at-closure: | Commit-as-machine-work: section 2.6's | | | machine work vs approval | "clean tree per closed UoW" makes it | | | | PART of closure; the human gate already | | | | approved the bytes; git_commit is non- | | | | destructive (push/rewrite still | | | | refuse). 2-commit corpus shape machine- | | | | run; real short hash backfilled into | | | | section (7) + the ledger row; non-git | | | | => "(not a git repository)" markers, | | | | never a fabricated hash. | | 4 | Decide which ledger | The TARGET workspace's end_to_end.md | | | | (U2.3 regex-heading detection, row for | | | | the UoW); anything else = a NAMED skip | | | | in the retro, never an error, never | | | | this repo's own ledger as a fallback. | | 5 | Decide whether closure keeps a | NO confirmation once invoked: design | | | human confirmation (--yes | section 4 draws no gate between RETRO | | | pattern) or is fully automatic | and CLOSED (ratify + escalation are the | | | | only two) and closure reads no input; | | | | the human act IS invoking `b3ubot | | | | close` (the between-phases checkpoint). | | | | The one subcommand with no Q-8-B guard. | | 6 | WorkflowDag row precursor | workflow_dags.jsonl (the egress/trail/ | | | | verifications precedent): one row per | | | | frozen instance, uow/step/kind/dag_uri/ | | | | status/frozen_at -- section 3's field | | | | list literally. | | 7 | Frozen enforcement in executing/ | refuse_if_frozen at execute_uow's | | | verifying | resume AND fresh-overwrite paths, | | | | verify_uow's per-pass write, and the | | | | repair reset -- proven against a | | | | TAMPERED engine record (the illegal | | | | route terminal CLOSED cannot see). | | 8 | CLI + tests + docs | `b3ubot close` (no provider, no | | | | prompts); 17 new tests incl. retro | | | | CONTENT assertions and the git-init toy | | | | fixture the brief required; design.md | | | | section 2.3/2.13 + ledger via the CLI. | +---+----------------------------------+------------------------------------------+ ``` ## Surprises / genuine findings 1. **The 17 new tests passed on the first run** -- worth recording precisely because it is unusual: the whole closure arc composed from already-proven parts (U2.3 table ops, U2.4 step_gdiff shell-out, U3.5 instance I/O, the U3.2 git verbs) with no new substrate behavior to discover. The composition-over-duplication discipline paying out measurably. 2. **The retro title doubled its UoW prefix** -- caught in the live CLI smoke, not the tests (step.19's finding 2, rhymed exactly): the corpus title form is "narrative retro for step.N (: )" but the skeleton's own title_line already leads with "step.N.txt -- <UoW>: ...", so the drafted title read "UT.1: step.1.txt -- UT.1: ...". Fixed by stripping the filename prefix. Two steps running, the live-smoke leg catches what assertion-shaped tests do not think to pin. 3. **step_gdiff needed no special toy-workspace handling** -- checked before designing rather than discovered after: its START is the parent of the commit that first ADDS step.N.txt, and in a machine-closed toy workspace that commit IS the closure's own work commit -- so the gdiff bounds to exactly the closure diff by construction. The shared script's range rule generalized to the engine's use unchanged. 4. **The freeze wall needed a text sniff, not a parse** -- a refusal guard that itself required bin/dagModel could not protect frozen evidence on a machine where the parser is not built (the step.18 parser-absent degradation lesson applied to a guard). The kv is engine-written in exactly one rendered form, so sniffing `(frozen_at = "` is honest; the substrate-parsed kv remains the real record. 5. **Q-19-C resolved on schedule**: step.19 deferred "per-pass verification graph snapshots?" to exactly this step; decided NO for V0 (the JSONL already carries every pass's verdicts; the frozen instance carries the final graph) -- recorded as Q-20-A, decided rather than re-deferred. ## Scope discipline U3.8 (M1 / automatic stage continuation), the PAL-narrated retro upgrade, T2+/T3/T4, Track B, Q-17-A, pushing anything anywhere: none started (G7). A closed UoW is terminal; the engine returns control between phases. The operator's explicit stop-after-U3.7 boundary is honored -- control returns after the retro pair. ## Numbers 517 passed / 11 skipped before -> 534 passed / 11 skipped after the build (+17 tests/test_orchestrator_closing.py) -> 536 passed / 11 skipped with this step's files in the corpus (+2 self-inclusive pickups). Zero regressions; zero existing tests edited (the stale-docstring sweep touched no pinned strings -- checked by grep before editing). Live smoke: 2 legs (git + non-git scratch workspaces), both rc=0, plus the terminal re-close refusal rc=1. End of retro.