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 (<UoW>:
   <title>)" 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.
