step.16.diff.txt -- narrative retro for step.16 (U3.3: SKELETON flow -- PAL-drafted step files; ground-truth probe prompts; LEANs presented for ratification) Executed: 2026-07-21, same session as the draft (single pass, the step.7/9/10/11/14/15 convention -- drafting.py, the error type, the package re-exports, the mock drafting fixtures, the `draft` CLI subcommand, the 18-test suite, the design.md/end_to_end.md edits, and the step file all built together). Companion artifact: step.16.gdiff.txt (automated, scripts/step_gdiff). Per the operator's explicit per-UoW checkpoint rule (same as step.14's/step.15's dispatch), this session STOPS after U3.3 -- U3.4 is not started. ## Plan vs actual ``` +---+----------------------------------+------------------------------------------+ | # | Planned (brief) | Actual | +---+----------------------------------+------------------------------------------+ | 1 | Module placement: drafting.py | app/orchestrator/drafting.py -- design | | | or skeleton.py, package | §2.3's ledger-ops list is "operations | | | precedent from U3.1 | the ORCHESTRATOR consumes", and U3.1's | | | | own INTENT->SKELETON edge rationale | | | | already named U3.3 as its driver; the | | | | module joins state_machine/persistence/ | | | | compiler as a per-concern peer. | | 2 | Decide the V0 probe scope | Top-level listing + README.md + | | | honestly, bounded | intent-named files (containment | | | | pre-checked; punctuation-stripped | | | | tokens), capped 8 files / 20k chars. | | | | Whole-tree rejected as unbounded egress | | | | (§2.4 "no more"; B-5); every content | | | | access via the trail-recorded tools. | | 3 | Record what was probed in the | STRONGER than asking the provider to: | | | drafted §(1) | the engine STAMPS its own mechanical | | | | ENGINE PROBE RECORD block into §(1) | | | | after the draft parses -- the on-disk | | | | skeleton shows what the engine actually | | | | read as an engine-written fact, never | | | | trusted from provider prose. Post-stamp | | | | re-parse guards the stamp itself. | | 4 | Parse-retry policy: bounded, | 1 initial + 1 fixed retry (MAX_ATTEMPTS | | | not d9's budget system | =2), the validation error AND the failed | | | | draft fed back, retry marker on the | | | | FIRST context entry (Q-4-B: makes the | | | | retry a distinct mock scenario; real | | | | adapters flatten context -- no change | | | | for them). Second failure refuses | | | | loudly naming the last error. d9 open. | | 5 | CLI placement vs U2.4's manual | Top-level `draft`, `ask`'s sibling (a | | | draft-step | PAL round trip with engine-state | | | | effects, not table machinery); `ledger | | | | draft-step` untouched and cross- | | | | referenced -- manual template vs | | | | PAL-drafted, names keep them honest. | | 6 | Steps-dir / step-number for a | /steps/ (the corpus | | | target workspace; never this | convention), always an explicit caller | | | repo's steps/ from tests | argument; next free N by regex-fullmatch | | | | on plain step.N.txt (retro siblings | | | | never counted); happy-path test asserts | | | | this repo's own steps/ byte-untouched. | | 7 | Human adapter path: exercised | Exercised, not skipped: injected I/O | | | or justified skip | drafts the same skeleton through the | | | | single content-bearing DONE (step.8's | | | | widened rule, kept local to drafting.py | | | | -- the orchestrator never imports the | | | | CLI layer). A human can BE the drafting | | | | provider. | +---+----------------------------------+------------------------------------------+ ``` Everything else held to plan. One small live finding (folded into the test, not papered over): the human-relay test's first intent didn't name hello.py, so the bounded probe correctly read only README.md and the relayed-content assertion failed -- the intent-named probe scope doing exactly what (2.2) says. Fixed by naming hello.py in the intent, not by widening the probe. One design point worth naming: drafting refuses an illegal INTENT->SKELETON move BEFORE the provider is called (proven with a MustNotBeCalledAdapter) -- no tokens are ever spent on a draft the state machine would refuse to record. ## Gate evidence ``` +------+---------------------------------------------------------------------+ | G1 | tests/test_orchestrator_drafting.py 18/18 PASS live: parseable | | | skeleton, SKELETON status, stamped probe record ABOVE the provider | | | narrative in S(1), LEAN-bearing S(5). | | G2 | Unparseable-twice: SkeletonDraftError naming "no numbered sections | | | found", spy counts exactly 2 run() calls, no steps/ created, state | | | still INTENT. Retry-success: attempts==2, error + failed draft | | | verbatim in the retry context, marker on the first entry. | | G3 | Human-relay drafts via injected I/O (content-bearing DONE); no | | | provider name/adapter construction/adapters-import in | | | app/orchestrator/ (grep gate run live). | | G4 | Trail shows list_dir/read_file probe calls + the write_file of the | | | skeleton; one egress record per attempt with probed content in | | | sent_context; this repo's own steps/ asserted untouched. | | G5 | Live CLI smoke against a scratchpad workspace: skeleton written, | | | "UT.1 INTENT -> SKELETON (step.1 recorded)", parseable on disk. | | G6 | 382p/11s baseline confirmed live -> 400p/11s after the build -> | | | 402p/11s once step.16.txt joined the corpus (+18 drafting tests, | | | +2 self-inclusive pickups). Zero regressions, zero tests edited. | | G7 | No ratify/amendment/approval code or prompt; no input() in | | | app/orchestrator/; U3.4+ mentions are scope annotations only. | | G8 | S11 ledger row updated THROUGH the `ledger` CLI (transition U3.3 | | | active --step step.16; close U3.3 PENDING) against the real | | | end_to_end.md. | | G9 | Porcelain clean per commit; retro pair via the SHARED scripts/ | | | step_gdiff; exactly 2 commits (work + gdiff-with-backfill). | +------+---------------------------------------------------------------------+ ``` ## Findings for later steps 1. **The engine-stamp pattern generalizes.** Anything mechanical the engine knows for certain (what was probed, what ran, what a gate returned) should be ENGINE-WRITTEN into the ledger artifact, with provider prose layered around it -- never solicited from the provider and trusted. U3.5's evidence recording and U3.7's retro closure should reuse exactly this shape (stamp into the parsed StepFile via Section.raw+body, re-parse as the guard). 2. **Refuse-before-the-provider-call is cheap and worth keeping.** validate_transition() run against the persisted record before any probe/PAL work means illegal moves cost zero tokens and leave zero egress. U3.4's ratify gate and U3.5's scheduler entry should keep this ordering (state legality first, spend second). 3. **The retry-marker-as-data duplication (drafting.RETRY_MARKER vs fixtures._RETRY_MARKER) is pinned by a test** because adapters may not import the orchestrator (C-2-B). If a third copy ever appears, promote the marker into app/pal (the shared layer both sides may import) instead of pinning a triangle. 4. **The drafted skeleton's quality ceiling is the fixture's, not the flow's.** The mock's skeleton is minimal-but-real; the first REAL drafting run (claude-code or human against a live workspace) is U3.4's natural smoke input -- ratifying an actually-drafted skeleton is the honest way to exercise the amendment loop. ## Numbers ``` +----------------------------------------+----------------------------+ | new files | 2 (app/orchestrator/ | | | drafting.py, tests/test_ | | | orchestrator_drafting.py) | | + step pair | 2 (steps/step.16.txt, | | | step.16.diff.txt) | | files edited | 6 (app/orchestrator/ | | | {errors,__init__}.py, | | | adapters/mock/fixtures.py, | | | app/cli.py, design.md, | | | end_to_end.md) | | drafting.py LOC | ~650 incl. docstrings | | new tests | 18 + 2 corpus pickups from | | | this step's own step.16.txt| | suite before -> after | 382p/11s -> 402p/11s | | regressions | 0 (zero tests edited) | | prompts introduced | 0 (ratify/approval is | | | U3.4) | +----------------------------------------+----------------------------+ ``` U3.3 done -- design.md §4's draft(PAL) arrow exists as concrete code: an intent plus a real, bounded, trail-recorded workspace probe goes through the PAL (any adapter -- mock, claude-code, or a human relaying by hand), comes back as a whole step-file skeleton, is validated refuse-loudly with one bounded retry, gets the engine's own probe record stamped into its ground-truth section, lands in the target workspace's steps/, and moves the UoW's engine state INTENT -> SKELETON. The skeleton then SITS there awaiting ratification, because the ratify gate is U3.4's and does not exist yet. Per the operator's explicit scope boundary, this session STOPS here. Ledger updated through the `ledger` CLI in the work commit; the work commit's hash is backfilled into end_to_end.md §11 and step.16.txt §7 in the gdiff commit -- the same 2-commit resolution steps 7-15 used. End of retro.