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 (/runs/.json, unconditional write). app/orchestrator/persistence.py mirrors data_home() exactly and follows the same one-JSON-file-per-record precedent (/orchestrator/.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 `/orchestrator/.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/ .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.