step.16.txt -- b3ubot U3.3: SKELETON flow -- PAL-drafted step files (ground-truth probe prompts; LEANs presented for ratification). design.md §4's INTENT --draft(PAL)--> SKELETON arrow, and §2.3's first ledger op (`draft(intent) -> skeleton`), as code: probe the workspace through U3.2's trail-recorded tool surface, have a provider draft a whole parseable skeleton, validate it refuse-loudly with ONE bounded retry, stamp the engine's own probe record into §(1), write it into the TARGET workspace's steps/, advance engine state. Stops here -- does NOT continue into U3.4 or any later P3 UoW. Step: 16 Parent: end_to_end.md §5 U3.3 bullet verbatim ("SKELETON flow: PAL-drafted step files (ground-truth probe prompts; LEANs presented for ratification)"). design.md §4 (INTENT --draft(PAL)--> SKELETON; "LEANs are presented for override exactly as the b3u steps present them") and §2.3's ledger-ops list ("draft(intent) -> skeleton (via PAL, §4 SKELETON state)") are what this step implements directly; §2.4 ("Context assembly is engine-owned") governs the probe. app/orchestrator/ (U3.1) is the package this grows; app/tools/ (U3.2) is the probe's substrate; app/ledger/ parser.py+emitter.py (U2.1/U2.2) are the validation/render machinery; app/ledger/retro.py's draft_step_skeleton() (U2.4) is the MANUAL sibling this deliberately does not replace. UoW: U3.3 Date: 2026-07-21 Origin: Operator directive 2026-07-21 (same dispatch shape as step.14's/step.15's): draft AND execute steps/step.16.txt for U3.3 in a single pass (the step.7/9/10/11/14/15 convention, NOT a skeleton-split), with the SAME explicit scope boundary -- stop after U3.3, do not continue into U3.4 or any later UoW; the operator is directing P3 one UoW at a time with per-UoW checkpoints. ## (0) Status EXECUTED in this pass. Not a skeleton-then-later-execute split -- app/orchestrator/drafting.py, the SkeletonDraftError addition, the package re-exports, the mock drafting fixtures, the `b3ubot draft` CLI subcommand, tests/test_orchestrator_drafting.py, the design.md/ end_to_end.md edits, and this step file were all built together, per the operator's explicit "draft AND execute (single pass)" direction. Per the operator's explicit scope boundary (Origin, above), this session STOPS after U3.3 -- U3.4 (ratify gate + amendment loop) is the next UoW but is NOT started here: the skeletons this flow drafts sit on disk in SKELETON state awaiting a ratification gate that does not exist yet. ## (1) Ground truth probed at drafting (outputs -> named writers) READ end_to_end.md §5's U3.3 row and design.md §4 + §2.3 in full. §4 names the arrow (INTENT --draft(PAL)--> SKELETON) AND the ratification contract this flow drafts FOR ("LEANs are presented for override exactly as the b3u steps present them" -- so the drafted §(5) must carry LEANs); §2.3's ledger-ops list names `draft(intent) -> skeleton (via PAL)` as the first op the orchestrator consumes. §2.4's "Context assembly is engine-owned ... composed into the envelope by b3ubot" is the probe's governing sentence. READ this repo's own steps/step.8.txt and step.15.txt §(1)/§(5) for what "ground-truth probe prompts" and "LEANs" mean in the REAL corpus: §(1) is READ/GREP/CONFIRMED entries of what was actually investigated (never invented), §(5) is Q-N-X questions each carrying a LEAN the human confirms or overrides at ratification. The drafted skeleton's shape mirrors exactly this (and the b3u corpus's same conventions, already surveyed exhaustively by U2.1). READ app/orchestrator/state_machine.py (U3.1): ("INTENT", "SKELETON") is a legal edge whose rationale string ALREADY names this step ("draft(PAL): a skeleton step file is proposed (U3.3 ...)"); persistence.py's advance_state() takes step= exactly for the INTENT -> SKELETON moment (its own docstring: "callers pass it on the INTENT -> SKELETON move"). U3.3 is the first real caller of that seam. READ app/pal/envelope.py + adapter.py + adapters/mock/ {adapter,fixtures}.py + adapters/human/adapter.py. Confirmed: mock routes scenarios off the FIRST context entry's content (Q-4-B -- so the intent goes first, and a retry must alter that entry to be separately scriptable); mock fixtures refuse loudly on unknown keys; the human adapter yields exactly ONE content-bearing DONE (step.7's real divergence), and step.8's Q-8-A widened acceptance rule (TEXT or content-bearing DONE) is the shape a provider-agnostic drafting flow must accept. READ app/cli.py in full: propose_ask()'s context-assembly precedent (intent first), its C-4-C unconditional egress.record_run() BEFORE any gate decision, the adapter= injection seam pattern (C-8-B), _make_adapter() as the ONE name->adapter mapping, and the ask/ledger subcommand structures the new `draft` subcommand sits beside. READ app/ledger/retro.py (U2.4): draft_step_skeleton() is the MANUAL template writer (FILL_IN slots, no provider, no probe, no engine state). Decision recorded in (2.6): the two COEXIST -- `b3ubot draft` (PAL) vs `b3ubot ledger draft-step` (manual template) -- U2.4's surface untouched. READ app/ledger/parser.py + emitter.py: parse_step_text() is the refuse-loudly validation gate (required header fields Step/Parent/Date/Origin; section (0) titled exactly 'Status'; strictly-increasing '## (N)' markers); Section.raw is what emit_step_file() reproduces for a PARSED StepFile, so the probe stamp must edit raw AND body coherently; the banner is runtime-injected (no BANNER.md on this box -> none emitted -> deterministic tests). READ app/tools/ (U3.2): list_dir/read_file are trail-recorded unconditionally (C-15-B) -- so probing through them makes "what did the engine look at" an AUDITED fact, not a docstring claim; write_file mkdirs parents (a fresh toy workspace's steps/ dir needs no special casing) and is C-4-B-scoped (the drafted skeleton physically cannot land outside the target workspace). READ tests/conftest.py (step.15): hermetic per-test $B3UBOT_HOME -- this step's engine-state/trail/egress writes in tests are sandboxed for free. CONFIRMED `git log --oneline -30` for the 2-commit phrasing (step.15's precedent, e6544fa/b6912b7). CONFIRMED baseline: full suite 382 passed / 11 skipped before any edit (matches step.15's reported final). LIVE FINDING (small, folded into the test): the human-relay drafting test's first intent didn't NAME hello.py, so the probe (correctly) read only README.md and the assertion on the relayed content failed -- the bounded intent-named probe scope doing exactly what it says. Fixed by naming hello.py in the intent, not by widening the probe. ## (2) What U3.3 delivers 2.1 **app/orchestrator/drafting.py** (new; the U3.1 package precedent -- drafting is an orchestrator op, design §2.3's ledger-ops list is "operations the ORCHESTRATOR consumes"): `draft_skeleton(workspace_root, uow, uow_title, intent, *, adapter, provider, date=, home=)` -- the full arc: engine-state legality check FIRST (refuses BEFORE any provider call if INTENT -> SKELETON is illegal from the record's current state; no tokens spent on an illegal move), step-number selection, bounded probe, engine-owned context assembly, PAL call, unconditional egress record, refuse-loudly validation with ONE bounded retry, engine probe-stamp into §(1), write via tools.write_file, advance_state(uow, "SKELETON", step="step.N"). Plus probe_workspace(), next_step_number(), and the ProbeRecord/ProbedFile/DraftResult dataclasses; all re-exported through app/orchestrator/__init__.py. 2.2 **The V0 probe scope (decision, justified):** top-level list_dir of the workspace root + README.md (if listed) + files the INTENT itself names (whitespace tokens, punctuation- stripped, containment PRE-checked so an intent merely mentioning an outside path is skipped, never a refused read aborting the draft), capped at 8 files / 20k chars per file (truncation marked). Bounded because a whole-tree read is unbounded egress of workspace content to a provider (§2.4's "Providers see what the policy layer allows -- no more"; B-5's spirit) and context budgeting is P6/U6.2's concern; this is the smallest scope that makes the drafted §(1) honest. Every content access goes through app/tools -- trail-recorded (C-15-B), workspace-scope-enforced (C-4-B). 2.3 **The probe record is ENGINE-STAMPED into §(1), never trusted from the provider (decision, justified):** the provider is instructed to write §(1) from the probed facts only, but a provider CAN hallucinate; after the draft parses, the engine prepends its own mechanical "ENGINE PROBE RECORD" block (LIST/ READ lines with sizes) into section (1) -- so the skeleton on disk always shows what the engine actually looked at, as a fact the engine itself wrote, mirroring how hand-written step files' §(1) works. Stamp edits Section.raw AND .body coherently; a post-stamp re-parse guards against the stamp itself breaking the draft (failure named as a drafting.py bug, not a provider failure). 2.4 **Validation + the bounded-retry V0 policy (decision, justified):** the draft must parse under app/ledger/parser.py, carry the ENGINE-assigned Step:/UoW: header values (the engine, not the provider, owns step-number selection and UoW identity), and have a §(1) to stamp. On failure: ONE fixed retry, with the validation error AND the failed draft fed back as context, and the retry marker appended to the first context entry (Q-4-B: that makes a retry a distinct, separately-scriptable mock scenario; real adapters flatten the context, so it changes nothing for them). A second failure raises SkeletonDraftError naming the last concrete error, what was NOT written, and the unchanged engine state. Deliberately NOT a budget system -- the repair-loop budget policy is design.md §5 d9, explicitly U3.6's open decision; 1-retry-then-refuse is the smallest honest interim. 2.5 **Step-number / steps-dir selection (decision, justified):** the steps location is `/steps/` -- the convention both this repo and the whole b3u corpus use, and the workspace_root is always an explicit caller argument (for the toy-workspace tests it is a tmp_path; NOTHING defaults to this repo). Next free N = max over plain `step.N.txt` names + 1 (regex-fullmatch, so `.diff.txt`/`.gdiff.txt` retro siblings and sub-letter files are never counted); no steps/ dir at all => N=1, dir created by the write itself. Refuses rather than overwrites if the target file appears between scan and write. 2.6 **CLI: top-level `b3ubot draft` (decision, justified):** placed as `ask`'s SIBLING, not under `ledger` -- it is a PAL round trip against a workspace with probing, egress recording, and engine-state effects, not a thin table wrapper; filing it under `ledger` would misfile an orchestrator operation as table machinery. COEXISTS with U2.4's `ledger draft-step`, which stays exactly what it was (the manual FILL_IN template writer); the naming keeps them honest (`draft` = the engine drafts via a provider; `ledger draft-step` = the machinery hands you a template). Same Q-8-B non-TTY refusal for `--provider human` as `ask`. Presentation-layer error handling only; draft_skeleton stays pure and raises. 2.7 **Provider-agnosticism by construction (§2.11):** the orchestrator never builds an adapter -- draft_skeleton REQUIRES an already-constructed Adapter plus a `provider` label for the egress trail; app/cli.py's _make_adapter() keeps owning the name->adapter mapping. The acceptance rule is step.8's widened Q-8-A rule (a distinct TEXT response OR a content-bearing terminal DONE), kept local to drafting.py (the orchestrator never imports the CLI layer), so the human-relay adapter is a first-class drafting provider: a human can BE the drafting provider, proven by test with injected I/O (C-7-B). 2.8 **Egress: every provider attempt recorded UNCONDITIONALLY** (the C-4-C posture, via egress.record_run, BEFORE validation): probed workspace content left the machine whether or not the draft validated; B-5's trail says so, per attempt (a failed-then-retried draft leaves TWO egress records). 2.9 **adapters/mock/fixtures.py**: drafting scenarios -- a deterministic `_skeleton(n, uow)` builder (parseable, SKELETON-status §(0), a §(1) to stamp, a LEAN-bearing §(5)), DRAFT_SKELETON_UT1 exported for the human-relay test, an unparseable draft, and retry-marked variants (the RETRY_MARKER literal duplicated as data -- adapters may not import the orchestrator, C-2-B/G5; a test pins the two literals together). 2.10 **app/orchestrator/errors.py**: SkeletonDraftError (base OrchestratorError), message always carrying the last concrete validation error. **__init__.py**: docstring + re-exports grown. 2.11 **tests/test_orchestrator_drafting.py** (18 new): happy path (parseable skeleton, SKELETON status, End-of-skeleton trailer; engine probe record stamped ABOVE the provider narrative in §(1); LEAN-bearing §(5); probe+write trail-recorded; egress recorded with the probed content in sent_context; engine state INTENT->SKELETON with step recorded and 2-entry history; this repo's OWN steps/ asserted byte-untouched); step-number selection (empty ws -> 1; step.1-3 + .diff/.gdiff/orphan-gdiff decoys -> 4); unparseable-twice refuses loudly naming the parser's own error, exactly 2 provider calls, nothing written, state NOT advanced; retry-success at attempts==2 with the error AND failed draft fed back and the marker on the first entry, 2 egress records; wrong-UoW header as a validation failure naming both sides; the RETRY_MARKER fixture-literal pin; already-SKELETON UoW refuses BEFORE any provider call (MustNotBeCalledAdapter); the human-relay leg (injected I/O drafts the same skeleton, probe context visible in the relayed block); CLI happy path / validation-failure rc=1 / --provider human non-TTY refusal. 2.12 **design.md §2.3**: a *Delivered state* paragraph under the ledger-ops list (the §2.7 precedent) -- draft() exists, ratify/record/close as engine ops remain U3.4/U3.5/U3.7. **end_to_end.md**: §5 U3.3 row gains the DONE step.16 clause in U3.1/U3.2's row style; §11 ledger row -> done/step.16 THROUGH the existing `ledger` CLI (dogfooding: `ledger transition U3.3 active --step step.16` then `ledger close U3.3 PENDING`; hash backfilled in the gdiff commit, step.14/15's shape). No new docs/*.md file -- the dispatch's deliverable list names none, and this file is the writeup. EXPLICITLY OUT OF SCOPE (named here per the operator's brief, not silently built -- and NOT continued into after this step, per the operator's explicit stop-here scope boundary): - U3.4 Ratify gate + amendment loop -- the drafted skeleton just SITS in SKELETON state awaiting ratification; no ratify UI or flow, no amendment round-trip, no approval prompt anywhere. -> next - U3.5 EXECUTING loop / scheduler -- nothing runs a ratified skeleton; the compiler (U3.1) is untouched. -> later - U3.6 VERIFYING / repair budgets -- d9 stays OPEN; the single fixed retry here is explicitly not d9's answer. -> later - U3.7/U3.8, Track B / P4. -> later ## (3) Verification gates G1 tests/test_orchestrator_drafting.py 18/18 PASS live: the full arc on a toy tmp_path workspace via mock; the drafted file parses under app/ledger/parser.py; §(1) carries the engine-stamped probe record above the provider narrative; §(5) carries a LEAN. G2 Bounded retry proven both ways live: unparseable-twice -> SkeletonDraftError naming "no numbered sections found", exactly 2 adapter.run() calls (spy-counted), no steps/ dir created, engine state still INTENT; unparseable-then-fixed -> success at attempts==2 with the parse error and the failed draft verbatim in the retry context and RETRY_MARKER on the first entry. G3 Provider-agnosticism: the human-relay adapter (injected output_fn/input_fn, zero real I/O) drafts the same skeleton through its single content-bearing DONE; the relayed request block visibly carries the probed workspace content. No provider name in app/orchestrator/ (grep gate: the only "mock"/"human" hits are workflow prose/comments, no adapter construction, no `import adapters` anywhere in the package). G4 Hygiene: probe reads + skeleton write all present in the tool trail (C-15-B); one egress record per provider attempt with the probed content in sent_context (C-4-C/B-5); this repo's own steps/ asserted untouched by the toy-workspace arc. G5 Live CLI smoke (not just tests): `b3ubot draft UT.1 "toy greeting tweak" "draft a goodbye line for hello.py" --date 2026-07-21` against a scratchpad workspace + isolated $B3UBOT_HOME -> skeleton written and printed, "UT.1 INTENT -> SKELETON (step.1 recorded)", parseable on disk with the stamped probe record. Confirmed live before committing. G6 Full suite before this step's edits: 382 passed / 11 skipped (confirmed live, matching step.15's reported final). After: 400p/11s pre-step-file; 402p/11s once this step.16.txt lands in the corpus (+18 drafting tests, +2 self-inclusive corpus pickups -- the steps 10-15 growth pattern). Zero regressions; zero existing tests edited. G7 Scope boundary held: no ratify/amendment/approval code or prompt anywhere in this step (no input() in app/orchestrator/; the only U3.4+ mentions in drafting.py are the d9 scope annotations); this session stops after U3.3. G8 §11 ledger row updated THROUGH the existing `ledger` CLI (transition -> active --step step.16, close -> done/PENDING), against the REAL end_to_end.md -- dogfooding, step.13/14/15's precedent. G9 b3ubot porcelain clean after each commit; retro pair via the SHARED v3_claude/scripts/step_gdiff (steps/step.16.gdiff.txt); exactly 2 commits (work + gdiff-with-hash-backfill-folded-in), step.7/9/10/11/13/14/15's precedent. ## (4) LOCKs C-16-A b3ubot porcelain clean after each commit. C-16-B THE ENGINE OWNS THE MECHANICAL FACTS OF A DRAFT: step number (next free N in the TARGET workspace's steps/), UoW identity, and the §(1) probe record (engine-stamped, never provider prose). A provider that renumbers or re-identifies is a validation failure, not a negotiation. C-16-C BOUNDED RETRY IS A CONSTANT, NOT A BUDGET SYSTEM: exactly 1 initial + 1 retry (MAX_ATTEMPTS=2); d9 (the repair-loop budget policy) stays OPEN for U3.6 -- nothing here may be extended into a budget knob without resolving d9 first. C-16-D NO PROVIDER NAME IN THE ORCHESTRATOR (§2.11): drafting takes a constructed Adapter + a provider LABEL; _make_adapter() in app/cli.py remains the one name->adapter mapping; app/orchestrator/ never imports adapters/. C-16-E EGRESS RECORDING PER ATTEMPT IS UNCONDITIONAL (C-4-C inherited): record_run() fires BEFORE validation on every attempt -- a rejected draft's sent context still left the machine and is still on the B-5 record. C-16-F NO SILENT SCOPE CREEP: U3.4-U3.8 named out of scope in (2) and confirmed not started (G7); the operator's explicit "stop after U3.3" direction is honored. C-16-G standing locks inherited: B-3 (no disclosure/no push); B-6 (deterministic pytest oracles gate this step); B-4 (zero keys anywhere -- mock/injected-I/O only in tests); B-5 (served, not weakened: the probe is the first real workspace-content-to-provider path beyond ask, and every byte of it is egress-recorded). ## (5) Open questions Q-16-A Should the probe grow recursive listing / grammar-aware file selection (e.g. read the files a named .sgr grammar includes)? LEAN: not before U3.5/U5.4 -- the V0 bound is deliberate (2.2); widen only when a real drafting arc on a real workspace demonstrably lacks context, and record the widening as its own decision (it is an egress-surface change, B-5). Q-16-B Should the drafted skeleton auto-append a ledger-table row (todo -> active) in the target workspace's own plan doc? LEAN: no for V0 -- the toy workspaces this flow drafts into have no end_to_end.md, and in THIS repo the ledger row is the operator's/dispatcher's move at promotion time (§12: "step.N is assigned when a UoW goes active"). Revisit at U3.7 (closure machinery), where the engine starts owning ledger writes end to end. Q-16-C Should validation also enforce the closing-section vocabulary (gates/LOCKs/acceptance present)? LEAN: no -- U2.1's own corpus survey found that vocabulary near-universal but NOT rigid (b3u step.8.txt differs), and the parser deliberately enforces marker shape + §(0) only; the RATIFIER judges content completeness (U3.4's gate), the engine judges form. Enforcing more here would make the validator stricter than the real corpus. ## (6) Acceptance - [x] app/orchestrator/drafting.py: probe -> context -> PAL -> egress -> validate (1 bounded retry) -> stamp -> write -> INTENT->SKELETON advance; refuses illegal state moves BEFORE any provider call (2.1, G1, G2). - [x] V0 probe scope: listing + README.md + intent-named files, capped, via the trail-recorded tool surface (2.2, G4). - [x] Engine-stamped probe record in the drafted §(1), above the provider narrative (2.3, G1, C-16-B). - [x] Step/UoW header validation + section-(1) requirement; the single-retry-then-refuse-loudly policy with the error and failed draft fed back (2.4, G2, C-16-C). - [x] Step-number selection: next free plain step.N.txt in the TARGET workspace's steps/; retro siblings never counted; tests never touch this repo's steps/ (2.5, G4). - [x] `b3ubot draft` CLI beside `ask`; coexists with `ledger draft-step` (untouched); Q-8-B non-TTY refusal (2.6, G5). - [x] Provider-agnostic: human-relay drafts via injected I/O; no provider name/import in the orchestrator (2.7, G3, C-16-D). - [x] Per-attempt unconditional egress records (2.8, G4, C-16-E). - [x] 18 new tests, zero regressions (382p/11s -> 400p/11s -> 402p/11s with this file) (2.11, G6). - [x] design.md §2.3 delivered-state note; end_to_end.md §5 row + §11 ledger via the `ledger` CLI (2.12, G8). - [x] Scope boundary held: U3.4-U3.8 not started; d9 open (G7, C-16-F) -- this session stops after U3.3. - [x] Retro pair: this file + step.16.diff.txt + step.16.gdiff.txt via the SHARED scripts/step_gdiff (G9). - [x] Exactly 2 commits (work + gdiff-with-backfill), not 3 (G9). ## (7) Hash backfill WORK commit (app/orchestrator/drafting.py + errors/__init__ + fixtures + cli + tests + design.md/end_to_end.md + this file): 3f8d0ba GDIFF commit (step.16.gdiff.txt via scripts/step_gdiff): (HEAD) End of step.