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 `<workspace_root>/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 <scratch-ws>
        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.
