step.14.txt -- b3ubot U3.1: State machine (design §4) + engine-state
               persistence + the RATIFIED->EXECUTING execution-DAG
               compiler -- P3's FIRST real orchestrator code (P2 is
               fully done: U2.1-U2.4, steps 10-13). Builds
               app/orchestrator/ from scratch: an explicit 8-state
               representation of design §4's Adaptive Programming
               Workflow, file-based per-UoW engine state under
               ~/.b3ubot (mirroring app/egress.py exactly), and a
               fixed V0 template compiler producing an execution-kind
               dag_model.Dag from a ratified step file. Stops here --
               does NOT continue into U3.2 or any later P3 UoW.

Step:     14
Parent:   end_to_end.md §5 U3.1 bullet verbatim ("State machine (design
          §4) + engine-state persistence; the EXECUTING entry compiles
          the ratified skeleton into an on-the-fly execution DAG
          (nodes=tasks, edges=deps, kv=state -- §2.13)"). design.md §4
          (the state diagram) + §2.13 (the workflow DAG substrate's
          lifecycle table) are the two sections this step implements
          directly. app/ledger/parser.py (StepFile, U2.1) is what "the
          ratified skeleton" IS in code today. app/ledger/
          table_transitions.py (U2.3) is the existing coarse ledger
          state machine this step's own investigation reconciles
          against design §4's fine-grained one. app/egress.py (U1.3)
          is the persistence-location precedent this step mirrors.
          dag/instances/execution_example.dag (U3.9a, step.6) is the
          one hand-authored precedent the compiler's fixed template
          matches the shape of.
UoW:      U3.1
Date:     2026-07-21
Origin:   Operator directive 2026-07-21: draft AND execute
          steps/step.14.txt for U3.1 in a single pass, like step.7/9/
          10/11/13 (NOT a skeleton-split like step.8), with an explicit
          scope boundary -- stop after U3.1, do not continue into U3.2
          or any later UoW, even though a prior agent (U2.2's session)
          autonomously continued through U2.3/U2.4 unprompted and that
          turned out fine on review. The operator is directing P3 one
          UoW at a time, deliberately, and wants to review before more
          is built on top of the orchestrator's first real code.

## (0) Status

EXECUTED in this pass. Not a skeleton-then-later-execute split --
app/orchestrator/ (state_machine.py, persistence.py, compiler.py,
errors.py), the three new test files, docs/orchestrator_state_machine.md,
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.1 -- U3.2 (tool surface + policy gates) is
the next UoW but is NOT started here, even though nothing structurally
prevents continuing (P2 is done, this step's own gates are green).

## (1) Ground truth probed at drafting (outputs -> named writers)

    READ    design.md §4 "The Adaptive Programming Workflow" (the state
            diagram: INTENT->SKELETON->RATIFIED->EXECUTING->VERIFYING->
            RETRO->CLOSED, SKELETON's own "(discard)" branch, the
            "amendments" edge from RATIFIED back to SKELETON, and the
            VERIFYING->EXECUTING "fail" edge for the bounded repair
            loop). Traced the ASCII diagram's box-drawing characters
            character-by-character to confirm the amendments edge's
            DIRECTION (RATIFIED -> SKELETON, not the reverse): the `^`
            arrowhead sits under SKELETON (flow arriving there) and the
            `+--amendments--+` line's right end joins the vertical bar
            falling FROM RATIFIED -- documented this reading explicitly
            in app/orchestrator/state_machine.py's own module docstring
            since the diagram never states it in prose.
    READ    design.md §2.13 "The workflow DAG substrate" (the lifecycle
            table: RATIFIED->EXECUTING compiles an execution DAG with
            nodes = context assembly/provider runs/tool calls/oracle
            gates/human gates; kv attrs carry engine state; "the v0
            grammar is deliberately permissive about kinds; kind
            vocabulary ... lives at the loader/OO-API layer" -- the
            sentence this step's V0-template scoping decision leans on
            directly).
    READ    dag/instances/execution_example.dag in full (the ONE
            hand-authored execution-kind instance, U3.9a/step.6): 4
            nodes (context_assembly/provider_run/human_gate/tool_call,
            each node's own `kind` equal to its `name`), 3 `precedes`
            edges (e_ca_pr/e_pr_hg/e_hg_tc), `path` attributes pointing
            at real, ALREADY-RUNNING code (app/cli.py:propose_ask
            etc). Confirmed oracle_gate was deliberately omitted
            ("no oracle tier exists in b3ubot's code yet") -- re-checked
            live (see below) whether that is STILL true post-P2; it is.
    READ    dag/dagModelPy/dag_model/entities.py, loader.py, renderer.py,
            __init__.py in full: Dag/Node/Edge/KeyValue dataclasses,
            from_dag()/from_bfgr() (subprocess to bin/dagModel + the
            ccs_python SCB walker), to_dag()/to_dag_text() (pure
            Python, byte-identical to cctdag). Confirmed dag_model is
            NOT pip-installed anywhere in this repo -- it is imported by
            inserting its directory onto sys.path, the same pattern
            dag/dagModelPy/tests/test_smoke.py itself uses; app/
            orchestrator/compiler.py mirrors that exact pattern rather
            than inventing a second import mechanism.
    READ    app/ledger/parser.py in full (StepFile/Section,
            parse_step_file/parse_step_text, KNOWN_HEADER_FIELDS)."the
            ratified skeleton" is a StepFile in today's code -- no
            separate "ratified" wrapper type exists, and none is built
            here (U3.4's job, if ever needed).
    READ    app/ledger/table_transitions.py in full (U2.3): the
            resolved todo/active/done/dropped rule set (todo->active
            requires step=, active->done requires commit=, done/dropped
            terminal), reasoned from end_to_end.md §0/§12. This is the
            COARSE state machine this step's own investigation (below)
            reconciles against design §4's fine-grained one.
    READ    end_to_end.md §0 (lines 20-38: Status vocabulary, "UoWs are
            promoted to steps/step.N.txt for execution (skeleton ->
            user ratification -> execute -> retro)") and §12 ("step.N
            is assigned when a UoW goes active"). These two sentences,
            read together with table_transitions.py's own rule set, are
            what RESOLVES the ledger-table-vs-design-§4 relationship --
            see (2.1) below and app/orchestrator/state_machine.py's own
            module docstring for the full citation trail.
    REREAD  steps/step.7.txt through step.13.txt's own headers,
            specifically to confirm the compiler's `_step_number()`
            extraction was necessary, not speculative -- see the LIVE
            FINDING below.
    READ    app/egress.py in full (U1.3): data_home()'s $B3UBOT_HOME/
            ~/.b3ubot convention, record_run()'s one-JSON-file-per-
            run_id shape (<home>/runs/<run_id>.json, unconditional
            write). app/orchestrator/persistence.py mirrors data_home()
            exactly and follows the same one-JSON-file-per-record
            precedent (<home>/orchestrator/<uow>.json) rather than
            inventing a new location or a new file format.
    READ    app/config/loader.py (U2.2) in full: config.txt's flat
            `key = "value"` shape, for a STATIC, hand-authored,
            single-namespace settings file (today: just `banner`).
            Compared its shape against engine state's actual shape
            (many dynamic per-UoW records, an append-only history list,
            nested structure) and confirmed config.txt's format does
            not fit without inventing a second nested syntax on top of
            it -- egress.py's JSON precedent is the better fit and is
            what this step uses; the reasoning is written out in full
            in persistence.py's own module docstring, not just
            asserted here.
    READ    app/ledger/errors.py + table_errors.py (U2.1/U2.3) for the
            refuse-loudly exception shape this step's own
            app/orchestrator/errors.py mirrors: a common base, named
            subclasses, no generic ValueError/KeyError, messages naming
            the concrete defect.
    READ    app/ledger/retro.py (U2.4) in full: confirmed
            draft_step_skeleton() is a MANUAL/CLI-driven step-file
            skeleton writer, NOT PAL-drafted (that's U3.3) -- named
            explicitly here so nothing in this step duplicates it.
    LIVE FINDING (folded into (2) below, not silently worked around):
            compiling steps/step.9.txt with a first-draft compiler that
            trusted `step.header_fields["Step"]` verbatim produced a
            `dag_name`/`description` corrupted with ~300 characters of
            unrelated prose. Investigated live: `app/ledger/parser.py`'s
            `_parse_header()` joins a header field's wrapped
            continuation lines with a single space, and 12 of this
            repo's own 13 real step files (all but step.13.txt) wrap
            free prose onto `Step:`'s continuation lines -- confirmed
            by running `parse_step_file()` against every real
            steps/step.N.txt and printing `header_fields["Step"]`'s
            length (294/486/362/435/2/321/342/201/482/406/383/208/322
            characters respectively for step.1/10/11/12/13/2/3/4/5/6/7/
            8/9 -- only step.13.txt's is the bare "13"). Fixed by
            extracting the LEADING integer via regex
            (`compiler.py._step_number()`) instead of trusting the
            field. Re-verified live against all 13 real files after the
            fix: clean `stepN` in every case (see (3) G4).
    CONFIRMED `git log --oneline -30` for exact commit-message phrasing
            (the `b3ubot step.N: Ux.y -- summary` / `b3ubot step.N: add
            step.N.gdiff.txt via scripts/step_gdiff` two-commit shape,
            step.13's own precedent, f6fd996/54eaba2), and `git show
            --stat f6fd996` to confirm the WORK commit bundles
            step.N.txt AND step.N.diff.txt together (not a 3rd commit)
            -- the shape this step follows exactly.

## (2) What U3.1 delivers

  2.1 **app/orchestrator/state_machine.py** (new): `STATES` (8: design
      §4's 7 named states + `DISCARDED`, this module's name for the
      diagram's unnamed "(discard)" branch off SKELETON),
      `TERMINAL_STATES` (`CLOSED`, `DISCARDED`), `validate_transition
      (current, target)` (refuses loudly via
      `IllegalStateTransitionError` on any of the 8x8-9 illegal pairs,
      mirroring `table_transitions.py.transition()`'s own message
      shape), `legal_targets(state)`. 9 legal edges total, including
      the RATIFIED->SKELETON "amendments" loop and the
      VERIFYING->EXECUTING repair-loop edge (both real design-diagram
      edges, represented even though the UoWs that OPERATIONALLY drive
      them, U3.4/U3.6, are out of scope here -- the state machine
      represents the FULL graph, not just this step's own slice of it).
      **The ledger-table-vs-design-§4 relationship, investigated and
      RESOLVED (not assumed)**, documented at length in this module's
      own docstring and mirrored in design.md §4 (2.6 below):
      end_to_end.md §0's "UoWs are promoted to steps/step.N.txt for
      execution (skeleton -> user ratification -> execute -> retro)"
      plus §12's "step.N is assigned when a UoW goes active", read
      together with table_transitions.py's own resolved rule set
      (todo->active requires step=, active->done requires commit=),
      conclude: ledger `active` is the superstate covering SKELETON
      through RETRO (a step.N.txt exists, work isn't CLOSED yet);
      `done` = CLOSED; `dropped` = DISCARDED (before or after a step.N
      was assigned); `todo` = INTENT. Confirmed empirically against
      this repo's own steps 7-13: every one collapses
      SKELETON-through-RETRO into a single-pass session, consistent
      with (not evidence against) the superstate reading.
  2.2 **app/orchestrator/persistence.py** (new): `EngineStateRecord`
      (uow/step/state/created_at/updated_at/history), `start_state()`
      (creates, refuses a duplicate), `load_state()` (the resume
      primitive), `advance_state()` (validates through
      `state_machine.validate_transition()` BEFORE writing -- an
      illegal move never reaches disk -- then appends to `history` and
      persists). File-based, one JSON record per UoW at
      `<data_home>/orchestrator/<uow>.json`, `data_home()` imported
      DIRECTLY from `app/egress.py` (not reimplemented) so
      `$B3UBOT_HOME` resolves identically everywhere in this repo.
      Motivated explicitly (module docstring) by this session's own
      recent, real precedent: the agent that built U2.2-U2.4 was
      interrupted by a process restart, and its work was only
      verifiable because everything already committed was file-based
      and inspectable -- the same property this module gives
      in-flight, uncommitted orchestrator runs.
  2.3 **app/orchestrator/compiler.py** (new): `compile_execution_dag
      (step: StepFile, uow: str) -> dag_model.Dag`. V0 FIXED TEMPLATE,
      confirmed against design §2.13's own "kind vocabulary ... lives
      at the loader/OO-API layer" framing and the "no orchestrator loop
      exists yet to run varying task graphs" scope fact (U3.5, out of
      scope): every ratified step file compiles to the SAME
      context_assembly/provider_run/human_gate/tool_call sequence
      execution_example.dag hand-authored, parameterized by uow/step-
      number/title only, never by the step's actual content. A 5th
      `oracle_gate` node is added ONLY when the step's own
      "Verification gates" section names a T3/CCS/grammar-round-trip
      check (`_names_oracle_gate()`, a regex over the section body) --
      confirmed live against all 13 real step files: NONE name one
      today (P5/U5.2 hasn't landed), so the branch is proven only via a
      synthetic test fixture, named explicitly as such rather than
      silently left unexercised against real data. Every node carries
      one `status = pending` kv attribute (design §2.13's "kv attrs
      carry engine state ... updated as tasks complete" -- set once
      here, left for U3.5's future scheduler); no `path` attribute
      (unlike execution_example.dag's hand-authored nodes, which
      correctly pointed at real code for an arc that already existed --
      no per-UoW code path exists yet for THIS compiler's output).
  2.4 **app/orchestrator/errors.py** (new): `OrchestratorError` (base),
      `IllegalStateTransitionError`, `UnknownEngineStateError` --
      mirrors app/ledger/errors.py's/table_errors.py's shape exactly.
  2.5 **app/orchestrator/__init__.py** (new): re-exports the package's
      public surface (STATES/TERMINAL_STATES/validate_transition/
      legal_targets/EngineStateRecord/start_state/load_state/
      advance_state/compile_execution_dag/the three error classes).
  2.6 **design.md** same-commit edit: a short paragraph appended to §4
      (after the CCS-integration-point paragraph, before the `---`
      divider) stating the ledger-vs-design-§4 resolution and pointing
      at docs/orchestrator_state_machine.md + the state-machine
      module's own docstring for the full reasoning.
  2.7 **end_to_end.md** same-commit edits: §5 U3.1 row -> gains a
      **DONE step.14** clause with a *Delivered:*-style summary (P3's
      table is a 2-column `| UoW | Title |` markdown table, not P1/P2's
      bulleted prose -- matched the ACTUAL live U3.9/U3.9a row
      convention of appending a bold "DONE stepN -- ..." clause inside
      the same cell, not P1/P2's separate bullet shape, since P3's
      table has no bullets to append to); §11 ledger row for U3.1
      updated THROUGH the existing `ledger` CLI (dogfooding, `ledger
      transition U3.1 active --step step.14` then `ledger close U3.1
      PENDING`), the same pattern step.13 used for its own row.
  2.8 **docs/orchestrator_state_machine.md** (new): the live-inventory
      write-up for all three modules -- the resolved ledger relationship
      with citations, the persistence format decision and its
      justification against config.txt, the compiler's V0 scope and the
      header-field LIVE FINDING, and the test-coverage summary.
  2.9 **tests/test_orchestrator_state_machine.py** (new, 26 tests):
      every one of the 9 legal edges (parametrized) succeeds; every
      documented illegal case (INTENT->EXECUTING, SKELETON->EXECUTING,
      EXECUTING->RETRO, terminal-state exits, self-transitions, unknown
      state names) raises `IllegalStateTransitionError` with specific
      message content; `legal_targets()`'s introspection shape;
      `STATES`/`TERMINAL_STATES` counts.
  2.10 **tests/test_orchestrator_persistence.py** (new, 12 tests):
      start_state()'s defaults/duplicate-refusal/explicit-initial-
      state; load_state()'s unknown-uow refusal and post-start
      agreement; advance_state()'s legal-chain success, illegal-move
      refusal (confirmed to write NOTHING to disk), unknown-uow
      refusal, created_at stability; THE full-resume-after-simulated-
      restart test (the done-when, literally -- see (3) G3).
  2.11 **tests/test_orchestrator_compiler.py** (new, 12 tests):
      compiling the REAL steps/step.9.txt (the P1-gates step, chosen as
      representative: real, non-trivial, full corpus shape) into the
      fixed 4-node template, node-kind==node-name and status=pending
      invariants, edge-chain shape compared directly against
      execution_example.dag's own node/edge sequence; the oracle_gate
      branch via a synthetic T3-naming fixture PLUS a negative case
      proving an ordinary gates section does not trigger it; a real
      round-trip through dag_model's OWN from_dag/to_dag (a live
      subprocess call to bin/dagModel, not mocked) confirming the
      compiled output is valid, loadable `.dag` text, including for the
      oracle_gate variant; a sweep compiling all 13 real step files
      without error.

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.2 Tool surface + policy gates (allowlist shell, git ops,
    approval for destructive -- the no-`--force` posture).       -> next
  - U3.3 SKELETON flow: PAL-drafted step files. draft_step_skeleton()
    (U2.4) is a MANUAL/CLI writer, not this.                    -> later
  - U3.4 Ratify gate + amendment loop (human-in-the-loop UI/flow) --
    the RATIFIED<->SKELETON edge is REPRESENTED in the state machine
    (2.1) but nothing here drives it operationally.              -> later
  - U3.5 The EXECUTING loop itself: scheduling from the compiled DAG's
    ready frontier, actually running tasks, recording evidence back
    into node kv state. U3.1 only COMPILES the DAG at RATIFIED->
    EXECUTING; it schedules/executes nothing.                    -> later
  - U3.6 VERIFYING: T0/T1 gate tiers, the verification-kind DAG, the
    bounded repair loop's BUDGET policy (d9, still OPEN) -- the bare
    VERIFYING->EXECUTING state edge is represented (2.1); the policy
    that would decide WHEN to take it is explicitly not decided here.
                                                                   -> later
  - U3.7 RETRO closure: freezing the DAG as evidence, hash-backfill
    machinery beyond what U2.4 already built.                    -> later
  - U3.8 M1 oracle: the full one-complete-UoW live arc.          -> later
  - Any change to Track B / design.md §3's SQLite data model (P4,
    independent, not started).                                   -> later

## (3) Verification gates

    G1  All 26 tests/test_orchestrator_state_machine.py cases PASS
        live: every legal edge (parametrized over the 9 real diagram
        edges) succeeds; every illegal case raises
        IllegalStateTransitionError with the specific message content
        asserted (not just "raises something") -- verified live: 26/26
        PASS.
    G2  All 12 tests/test_orchestrator_compiler.py cases PASS live,
        including the REAL round-trip through dag_model's own
        from_dag/to_dag (a live subprocess call to bin/dagModel) and
        the direct shape comparison against dag/instances/
        execution_example.dag -- verified live: 12/12 PASS.
    G3  tests/test_orchestrator_persistence.py's
        test_full_resume_after_simulated_process_restart is the
        done-when, literally: state is written through one call chain
        ("process A"), then read back through a COMPLETELY FRESH
        load_state() call with no shared in-memory object ("process
        B"), the resumed state is correct (state/step/full history),
        AND process B successfully continues the arc to CLOSED from
        the resumed point -- proving the resumed state is not just
        readable but ACTIONABLE. All 12 tests/test_orchestrator_
        persistence.py cases PASS live: 12/12.
    G4  The header-field LIVE FINDING (1) is fixed and re-verified:
        `compile_execution_dag()` run against all 13 real
        steps/step.N.txt files produces a clean `stepN` (no embedded
        prose) in every `dag_name`/description -- verified live via
        tests/test_orchestrator_compiler.py::
        test_compiled_dag_survives_the_whole_real_step_corpus (13/13
        files, 4-5 nodes each, edge count == node count - 1).
    G5  The oracle_gate branch fires ONLY on a real T3/CCS-naming gate
        section, never merely because a "Verification gates" section
        exists -- verified live via the paired positive (synthetic T3
        fixture -> 5 nodes) and negative (steps/step.9.txt, which DOES
        have a Verification gates section but names no T3/CCS check ->
        4 nodes) tests, both PASS.
    G6  Full suite before this step's edits: 277 passed/11 skipped
        (confirmed live, matching step.13's own reported baseline).
        After the 3 new test files landed: 329 passed/11 skipped (+52
        -- the 26+12+12=50 new orchestrator tests, plus 2 more from the
        step-file parser's/emitter's own self-inclusive corpus
        parametrization picking up this step's own steps/step.14.txt,
        the same growth pattern steps 10-13 documented for their own
        predecessors). A REAL, live-data collision found and fixed
        along the way, not silently worked around (the SAME recurring
        pattern step.13's own retro named as a standing risk):
        dogfooding U3.1's own ledger row through `done` (2.7) broke
        tests/test_ledger_table_roundtrip.py::
        test_structural_round_trip_after_mutating_a_row (step.13's own
        fix, which had repointed this test at U3.1 as "the next real
        todo row" after U2.4 went done) -- confirmed live (the suite
        went red immediately after the dogfooding CLI commands ran),
        fixed by repointing the test at U3.2 (the next real todo row
        as of this step) instead of loosening or deleting it. Final:
        329 passed/11 skipped, 0 regressions.
    G7  end_to_end.md's own U3.1 bullet, LITERALLY: "State machine
        (design §4) + engine-state persistence; the EXECUTING entry
        compiles the ratified skeleton into an on-the-fly execution DAG
        (nodes=tasks, edges=deps, kv=state -- §2.13)" -- G1-G3 together
        ARE this gate (state machine: G1; persistence: G3; the
        compiler producing nodes=tasks/edges=deps/kv=state: G2).
    G8  §11 ledger row for U3.1 updated THROUGH the existing `ledger`
        CLI (not the raw table_transitions API directly) -- `.venv/
        bin/python -m app.cli ledger transition U3.1 active --step
        step.14` then `... ledger close U3.1 PENDING`, against the REAL
        end_to_end.md -- confirmed via `git diff --stat` before
        committing (a 1-line change to the §11 row, plus the §5 bullet
        edit).
    G9  Scope boundary held: `grep -n "U3\.[2-8]" app/orchestrator/*.py`
        -> 15 hits across compiler.py/state_machine.py/__init__.py,
        every one read individually and confirmed to be a SCOPE-
        BOUNDARY ANNOTATION ("out of scope for U3.1", "U3.5, not built
        here", naming which future UoW will implement something) --
        never an actual implementation of any of those UoWs' own
        behavior (no tool-surface code, no PAL-drafting code, no
        ratify-gate UI, no scheduler, no gate-tier wiring, no repair-
        loop budget policy anywhere in this step's new files) --
        confirmed live before committing. This session stops here;
        U3.2 is not started.
    G10 b3ubot porcelain clean after each commit; retro pair via the
        SHARED v3_claude/scripts/step_gdiff (steps/step.14.gdiff.txt);
        exactly 2 commits (work + gdiff-with-hash-backfill-folded-in),
        step.7/9/10/11/13's own precedent.

## (4) LOCKs

    C-14-A  b3ubot porcelain clean after each commit.
    C-14-B  NO SILENT UNDER-SCOPING: the compiler's V0 fixed-template
            scope (2.3) is confirmed EXPLICITLY against design.md
            §2.13's own "kind vocabulary lives at the loader/OO-API
            layer" framing, not asserted without support -- see (1)/
            (2.3) and the module's own docstring.
    C-14-C  NO SILENT SCOPE CREEP: U3.2-U3.8 are named out of scope in
            (2) and confirmed NOT started (G9) -- the operator's
            explicit "stop after U3.1" direction (Origin) is honored,
            not treated as a suggestion. This is the central discipline
            this step's own brief asked for.
    C-14-D  STATE CHANGES VALIDATE BEFORE THEY PERSIST:
            `persistence.advance_state()` calls
            `state_machine.validate_transition()` BEFORE writing
            anything to disk -- an illegal move never reaches a
            persisted record (verified live,
            test_advance_state_illegal_move_raises_and_writes_nothing).
    C-14-E  NO NEW DATA HOME, NO NEW DB: `app/orchestrator/
            persistence.py` imports `data_home()` DIRECTLY from
            `app/egress.py` (not reimplemented) -- verified live via
            `grep -n "data_home\|B3UBOT_HOME" app/orchestrator/
            persistence.py` -> only the import and its call sites, no
            second env-var name or default path anywhere. Track B's
            SQLite pipeline (P4) is untouched (verified: `grep -rl
            "sqlite3\|import.*sql" app/orchestrator/` -> empty).
    C-14-F  standing locks inherited: B-3 (no disclosure/no push); B-6
            (deterministic pytest oracles gate this step; no AI review
            tier). B-1/B-2/B-4/B-5 are moot -- this step invokes no
            cppcc directly (only the already-built dag/cppcc/bin/
            dagModel binary, via the pre-existing dag_model loader), no
            provider, no egress beyond the pre-existing egress.py
            module this step imports but does not modify.

## (5) Open questions

    Q-14-A  Should `EngineStateRecord` eventually gain a pointer to the
            compiled execution DAG's `.dag` file path, so a resumed
            process can locate it without re-deriving the name from
            `compile_execution_dag()`'s own naming scheme? LEAN: not
            yet -- no caller exists yet that NEEDS to locate a
            previously-compiled DAG from a resumed record (U3.5, the
            scheduler, is what would need this); adding the field now
            would be speculative. Revisit when U3.5 is built and
            actually needs it.
    Q-14-B  Should the oracle_gate detection heuristic
            (`_ORACLE_GATE_RE`) be widened beyond T3/CCS/grammar-
            round-trip wording, e.g. to catch a future gate phrased
            differently but meaning the same thing? LEAN: not yet --
            zero real step files exercise this branch today (1), so
            widening the regex now would be tuning against fixtures
            this repo doesn't have. Revisit once P5/U5.2 lands and real
            step files start naming T3 gates in their own words; keep
            the regex's test coverage (the synthetic fixture) as the
            place a future widening gets proven against.
    Q-14-C  Should `compile_execution_dag()` write its output `.dag`
            file to disk itself (e.g. under `dag/instances/`), or stay
            a pure in-memory `Dag`-returning function with the CALLER
            deciding where/whether to persist it? LEAN (built this
            way): stay pure -- `dag_model.to_dag()` already exists and
            is a one-line call for any caller that wants a file; a
            compiler that ALSO decides a file location would be making
            a policy choice (where do execution DAGs for in-flight UoWs
            live? one file? a directory per uow?) that belongs to U3.5
            (the actual scheduler, which is the first real consumer of
            "where do I read/write DAG state") rather than to the
            template-compiling function itself.

## (6) Acceptance

    - [x] app/orchestrator/state_machine.py: STATES (8), TERMINAL_STATES
          (2), validate_transition, legal_targets, the resolved
          ledger-vs-design-§4 relationship documented in-module (2.1,
          G1, G7).
    - [x] app/orchestrator/persistence.py: EngineStateRecord,
          start_state/load_state/advance_state, ~/.b3ubot/orchestrator/
          <uow>.json, mirrors app/egress.py's data_home() exactly (2.2,
          G3, G7, C-14-D, C-14-E).
    - [x] app/orchestrator/compiler.py: compile_execution_dag, V0 fixed
          4-5-node template, oracle_gate conditional, the header-field
          LIVE FINDING fixed (2.3, G2, G4, G5, G7, C-14-B).
    - [x] app/orchestrator/errors.py + __init__.py (2.4, 2.5).
    - [x] design.md §4: the ledger-relationship resolution paragraph
          (2.6).
    - [x] end_to_end.md: §5 U3.1 -> DONE step.14 with a *Delivered:*-
          style clause matching P3's actual table shape; §11 ledger row
          -> done/step.14/PENDING via the existing `ledger` CLI (2.7,
          G8).
    - [x] docs/orchestrator_state_machine.md (2.8).
    - [x] tests/test_orchestrator_state_machine.py -- 26/26 PASS (2.9,
          G1).
    - [x] tests/test_orchestrator_persistence.py -- 12/12 PASS,
          including the literal resumability done-when (2.10, G3).
    - [x] tests/test_orchestrator_compiler.py -- 12/12 PASS, including
          the real dag_model round-trip (2.11, G2).
    - [x] Zero regressions: 277p/11s -> 329p/11s, one pre-existing test
          fixed (not loosened) after a real live-data collision with
          this step's own ledger dogfooding, the same recurring pattern
          step.13's own retro named (G6).
    - [x] Scope boundary held: U3.2-U3.8 named out of scope and
          confirmed not started (G9, C-14-C) -- this session stops
          after U3.1.
    - [x] Retro pair: this file + step.14.diff.txt + step.14.gdiff.txt
          via the SHARED scripts/step_gdiff (G10).
    - [x] Exactly 2 commits (work + gdiff-with-backfill), not 3 (G10).

## (7) Hash backfill
WORK commit (app/orchestrator/ + tests + docs + design.md/end_to_end.md + this file):  f66b53e
GDIFF commit (step.14.gdiff.txt via scripts/step_gdiff):  (HEAD)

End of step.
