step.18.txt -- b3ubot U3.5: EXECUTING loop -- scheduling from the execution DAG's ready frontier; tool acting + evidence recorded into the step file AND node kv state. design.md §4's "Everything between the gates is machine: context assembly, provider runs, tool acting, oracle invocation, evidence recording" and §2.13's "during EXECUTING/VERIFYING node kv state updated as tasks complete; the DAG IS the live progress surface -- what may run next = the ready frontier" -- as code. Also resolves Q-15-B (child env scrub). Stops here -- does NOT continue into U3.6 or any later P3 UoW. Step: 18 Parent: end_to_end.md §5 U3.5 bullet verbatim ("EXECUTING loop: scheduling from the execution DAG's ready frontier; tool acting + evidence recorded into the step file AND node kv state"). design.md §4 (the RATIFIED->EXECUTING->VERIFYING span and the "materialized form is DAG instances" paragraph) + §2.13's lifecycle table are what this step implements directly; §2.12's "file trees for ... workflow DAG instances" governs where instances live. app/orchestrator/ (U3.1-U3.4) is the package this grows; app/tools/ (U3.2/U3.4) is the surface every node handler acts through; dag/dagModelPy + dag/cppcc/bin/dagModel are the substrate the instances are written to and verified against. UoW: U3.5 Date: 2026-07-21 Origin: Operator directive 2026-07-21 (same dispatch shape as step.14-17's): draft AND execute steps/step.18.txt for U3.5 in a single pass (the step.7/9/10/11/14-17 convention, NOT a skeleton-split), with the SAME explicit HARD scope boundary -- stop after U3.5, do not continue into U3.6 or any later UoW; the operator directs P3 one UoW at a time with strict per-UoW checkpoints. ## (0) Status EXECUTED in this pass. Not a skeleton-then-later-execute split -- app/orchestrator/executing.py, the ExecuteError/ExecutionHaltedError additions, the Q-15-B env scrub in app/tools/shell.py, the compiler's ASCII-description fix (a live finding, see §(1)), the mock execution fixture, the `b3ubot execute` CLI subcommand, tests/test_orchestrator_executing.py, the env-scrub tests grown into tests/test_tools_shell.py, the design.md / end_to_end.md / docs/tool_surface.md / docs/orchestrator_state_machine.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.5 -- U3.6 (VERIFYING tiers / verification-kind DAG / d9 repair budget) is the next UoW but is NOT started here: an executed UoW lands in VERIFYING and just sits there; nothing runs gates. ## (1) Ground truth probed at drafting (outputs -> named writers) READ design.md §4 in full (the "Everything between the gates is machine" list is exactly this step's handler set; "the orchestrator schedules from the DAG's ready frontier, writes task state into node kv attributes") and §2.13's lifecycle table (row 2 is this step's contract verbatim). §2.12's storage wording ("file trees for ledgers, artifacts, AND workflow DAG instances") drove the instance-location decision (2.2). READ app/orchestrator/ in full: state_machine.py (the RATIFIED->EXECUTING and EXECUTING->VERIFYING edges both exist with rationale strings naming U3.5; Q-17-B's RATIFIED->SKELETON stays undriven -- confirmed NOT needed here, no state-machine change made); persistence.py (the resume primitive this step's failure semantics lean on); compiler.py (U3.1's fixed 4-5-node template, `status = pending` kvs, `precedes` edges -- consumed as-is, the template deliberately NOT enriched); drafting.py (the bounded probe reused for context_assembly; the C-4-C egress-record-before-validation posture mirrored); ratify.py (the §(0) engine-stamp pattern with post-stamp re-parse guard, reused for the execution record; the legality-first lesson). READ dag/dagModelPy/dag_model/ (entities/loader/renderer): from_dag shells out to dag/cppcc/bin/dagModel -- CHECKED, not assumed: the binary IS built in this environment and from_dag -> to_dag_text round-trips execution_example.dag byte-identically (verified live before designing). Droppings observed live: a parse leaves .bfgr/.fgr/.lis/.urt/.log beside the input. The renderer writes latin-1 (its own documented rationale) while tools.write_file writes utf-8 -- the encoding seam behind this step's compiler finding (below). READ dag/instances/execution_example.dag: the kv evidence shape ((status = done), (provider = mock), (approved = true)) this step's node facts follow -- and the detail that the hand-authored instance deliberately writes "design.md section 4", never the section-sign character. READ app/tools/ in full: shell.py (_execute shared by run_shell/run_gate -- the one place the Q-15-B scrub belongs; exit-as-data already anticipated "the U3.5 scheduler needs that verdict as data"); workspace.py (write_file's C-4-B scope enforcement -- relied on AT the apply); trail.py (logged_call -- every handler action lands on the trail for free). READ steps/step.15.txt §(5) Q-15-B verbatim (the LEAN said "revisit at U3.5 -- that is the moment inherited env becomes a provider-reachable surface"; this step is that moment, resolved in 2.8) and design.md §2.10's B-4/B-5 lock table. READ app/cli.py in full (the ask/draft/ratify subcommand row `execute` joins; the Q-8-B non-TTY refusal pattern; --yes precedents) and adapters/mock/fixtures.py (the Q-4-B first-entry routing rule the execution scenario must respect). CONFIRMED `git log --oneline -30` for the 2-commit phrasing (step.17's precedent, 8e14637/1217e79). CONFIRMED baseline: full suite 443 passed / 11 skipped before any edit (matches step.17's reported final). CONFIRMED live smoke before committing: mock draft + ratify --yes + `b3ubot execute UT.1 --yes` in a scratch workspace with isolated $B3UBOT_HOME -> instance written beside the skeleton, HUMAN GATE diff presented, hello.py applied, ENGINE EXECUTION RECORD in §(0) on disk, engine record history [INTENT, SKELETON, RATIFIED, EXECUTING, VERIFYING], round-trip "verified" through the real bin/dagModel, zero droppings left in steps/. ## (2) What U3.5 delivers 2.1 **app/orchestrator/executing.py** (new; the package's per-concern peer of drafting.py/ratify.py): `execute_uow(workspace_root, uow, *, adapter, provider, output_fn, input_fn, assume_yes=, home=)` -- the full arc: state legality FIRST (an EXECUTING-state record RESUMES; any other state must pass validate_transition(state, "EXECUTING"), so only RATIFIED enters -- zero reads on an illegal move), compile via U3.1's compile_execution_dag (the compiler consumed AS-IS: the V0 template deliberately not enriched), instance written, ready-frontier loop with per-node-kind handlers, node kv persistence on every change, §(0) evidence stamp, advance to VERIFYING. Plus the generic engine (run_ready_frontier / ready_frontier / node_status), ExecuteError + ExecutionHaltedError in errors.py, all re-exported through app/orchestrator/__init__. 2.2 **Where the instance lives (decision, justified):** `steps/.execution.dag` BESIDE the skeleton -- the U3.4 amendments-file precedent applied to the next per-step artifact. §2.12 names "file trees for ... workflow DAG instances"; §4 calls the frozen instance "the step's evidence graph" -- per-step evidence is workspace content that travels with the step file. NOT a workspace-level dag/ dir (in the one real workspace, this repo, `dag/` IS the substrate -- name collision), and NEVER this repo's own dag/instances/ (the substrate's regression corpus; tests assert it untouched). 2.3 **How node kv state is persisted (decision, justified):** full rewrite (to_dag_text) through the trail-recorded tools.write_file on EVERY node status change -- `running` written BEFORE the handler fires (a crash mid-node leaves an honest marker in the file), then done/failed/declined plus the node's fact kvs (started_at/finished_at timestamps; provider + run_id on provider_run; approved on human_gate; applied on tool_call; gate + exit_code on oracle_gate; files_probed on context_assembly -- §2.13's "status, run ids, timestamps" and the execution_example.dag attr shape). The honest files-as-truth V0: the .dag text is the single truth; 4-5-node instances make a delta format complexity without a consumer. 1 initial + 2-per-node writes, trail-asserted in the tests. 2.4 **Substrate round-trip verification (the cross-repo dependency, handled honestly both ways):** bin/dagModel IS available here, so the written instance is verified (from_dag -> to_dag_text, BYTES-identical) at initial write and at loop completion, with the parser droppings removed after each parse (the text is the truth; a stale .bfgr sibling that stopped tracking kv rewrites would be a lie). Parser absent => writes degrade to a NAMED "skipped (parser not found)" outcome stamped into the evidence -- but RESUME refuses loudly naming the build command, because reading state back IS parsing. 2.5 **Per-node-kind V0 semantics (the core design work, mapped onto ONLY what exists):** context_assembly = U3.3's probe_workspace through the trail-recorded tool surface, intent = the ratified skeleton's title line + §(2) deliverables body (the section whose tokens name the files the work touches; honest fallback to title-only if no §(2)). provider_run = ONE PAL call through the INJECTED adapter (no provider name in the orchestrator, §2.11): deterministic first context entry "EXECUTE : " (Q-4-B routing -- the mock scenario keys off it), an instruction block fixing the V0 proposal shape (line 1 = the target's workspace-relative path, then the COMPLETE new file content -- the proven ask arc widened to let the provider pick the file), the ratified skeleton text (the plan; it was PAL-drafted so it already legitimately left the machine once), and the probe; the step.8 TEXT-or-content-bearing-DONE acceptance rule; egress-recorded UNCONDITIONALLY before the proposal is parsed or judged (C-4-C). human_gate = the in-loop proposal review (design §4's gate applied inside EXECUTING, distinct from ratify): unified diff through output_fn, approve/ decline through input_fn (C-7-B injectable end to end), EOF = decline, assume_yes = the --yes bypass. tool_call = the gated apply, tools.write_file of the approved proposal -- C-4-B enforced AT the write, so an escaping proposal path FAILS the node (spy-tested with ../evil.txt). oracle_gate = run_gate on the NAMED convention script scripts/oracle_gate.sh, ONLY when the compiled DAG contains the node (the U3.1 compiler adds it only for skeletons naming a T3/CCS gate); exit-as-data: 0 => done with exit_code kv, non-zero/timeout => failed + halt. 2.6 **The frontier engine is GENERIC over the DAG, not the template:** ready = every runnable node whose `precedes` predecessors are all done (edges of other kinds are annotations and never constrain scheduling); sequential execution in declaration order (deterministic; parallelism has no current consumer -- named, not smuggled); a stalled DAG (nothing ready, not all done -- a cycle) and an unknown node kind both refuse loudly. Proven against a synthetic branching diamond, a multi-root join, an annotates back-edge, and a cycle -- the done-when's own point of scheduling from a frontier. 2.7 **Failure semantics (V0, decision, justified):** halt loudly, leave a resumable honest state -- NOT d9's repair budget (no retry counting, no cost ceiling; that is U3.6's open decision). A node failure marks failed + an `error` kv (ASCII-sanitized, truncated) in the instance; a human decline marks declined + approved=false (a decline is a decision, not an error); both raise ExecutionHaltedError (node_name/node_status attached) and the engine state STAYS EXECUTING. Re-running execute_uow resumes: done nodes skipped, failed/declined/running re-run. A resumed human_gate/tool_call whose in-memory proposal is gone recovers it from the EGRESS record the provider_run node's own run_id kv names -- the B-5 audit trail doubling as the resume source (proven: the resume-after-decline test passes a MustNotBeCalledAdapter -- zero provider calls, applied result byte-identical). 2.8 **Q-15-B RESOLVED (app/tools/shell.py): scrubbed child env, not inheritance.** _execute (shared by run_shell AND run_gate) now passes env=scrubbed_env(): the parent environment MINUS every variable whose NAME matches API_KEY/ACCESS_KEY/SECRET/TOKEN/ PASSWORD/PASSWD/CREDENTIAL/PRIVATE_KEY (case-insensitive). Justification against B-4: provider keys are env-only, so inheritance handed the ONE copy to every allowlisted child -- tolerable while every caller was the developer/test suite (step.15's LEAN), a provider-reachable surface the moment this step's loop started running work downstream of provider proposals (a proposed gate script could env-dump). DENY-BY-PATTERN, not allow-by-name: the venv/make/pytest legitimately need most of the env (PATH/HOME/VIRTUAL_ENV/ B3UBOT_HOME/...), and an allowlist would break real builds unpredictably; credentials are nameable by convention. Deliberately no bare "AUTH" match -- SSH_AUTH_SOCK is a socket path a child build may need, not a credential value. Proven with a REAL python3 -c child asserting the key is ABSENT (and a non-secret var visible), for run_shell AND run_gate. 2.9 **End-of-loop state (decision, justified):** completion advances EXECUTING -> VERIFYING with verification itself ABSENT, named as U3.6's job. design §4's edge fires when "tool calls complete"; leaving a completed execution in EXECUTING would be a lie, and VERIFYING-with-nothing-running mirrors exactly how U3.4 left RATIFIED sitting until this step gave EXECUTING an entry point. The §(0) ENGINE EXECUTION RECORD (timestamp, instance path + round-trip verdict, per-node outcome lines, the state advance) is stamped ONCE, on completion, via the emitter/parser machinery with the post-stamp re-parse guard (the step.16/17 pattern; a double-stamp is guarded by a marker check). A resumable halt stamps NOTHING into §(0): §2.13 names the DAG instance, not the step file, as the LIVE progress surface -- the step file gets the durable ledger fact. 2.10 **CLI: top-level `b3ubot execute`** (ask/draft/ratify's sibling): workspace_root + uow + --provider + --yes. Q-8-B non-TTY refusal unless --yes (the in-loop human gate needs input; --yes auto-approves it, the ask --yes rule); --provider human needs a TTY REGARDLESS of --yes (the relay itself is interactive). Presentation-layer error handling only; execute_uow stays pure and raises; a halted loop presents and exits 1 with the state left resumable. 2.11 **tests/test_orchestrator_executing.py** (32 new): happy path (VERIFYING + full history; proposal applied; instance beside the skeleton with ZERO droppings; every node kv done with its facts, in the returned result AND in the file; §(0) stamp once, still parseable, ratification record preserved; round-trip "verified" through the real parser; egress record's first entry; 9 trail-counted instance rewrites; this repo's own steps/ AND dag/instances/ untouched); human gate (diff presented; decline halts resumably with declined/approved=false kvs; EOF = decline; resume-after-decline recovers the proposal from egress with a MustNotBeCalledAdapter and completes); failure (provider exception -> failed + error kv in the file, state EXECUTING, hello.py untouched; failed node re-runs on resume; malformed proposal fails AFTER egress recording; ../evil.txt proposal fails at the apply, nothing written outside); legality (SKELETON-state refuses before anything with zero provider calls; unknown UoW; EXECUTING-without-instance refuses with named repair); the generic frontier (diamond order, multi-root, annotates-edge ignored, cycle refused, unknown kind refused, ready_frontier unit); oracle_gate (green completes 5 nodes with exit_code=0; red halts with exit_code=1 in the file -- and the change WAS applied, the gate verdict being about the applied state); CLI (non-TTY refusal, human- needs-TTY-even-with---yes, unattended --yes success, error presentation). Plus 4 Q-15-B tests grown into tests/test_tools_shell.py (child cannot see the key; non-secret passes; the pattern family incl. SSH_AUTH_SOCK kept; run_gate child scrubbed too). 2.12 **Docs (same commit):** end_to_end.md §5 U3.5 row -> DONE in the U3.2-U3.4 row style; §11 ledger THROUGH the ledger CLI (transition U3.5 active --step step.18; close U3.5 PENDING; hash backfilled in the gdiff commit -- the step.13-17 shape). design.md §2.3 (record(evidence) delivered), §2.7 (DAG wiring + env scrub delivered-state note), §2.13 (delivered-state note: lifecycle rows 1-2 are code). docs/tool_surface.md (title + wiring + the env-scrub posture bullet). docs/orchestrator_state_machine.md (the live-inventory delivered-state updates). Every stale "U3.5 pending" docstring updated (app/tools/__init__.py, shell.py, gitops.py, app/orchestrator/__init__.py, compiler.py, ratify.py's post-ratify print now names `b3ubot execute`). No new docs/*.md file -- 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.6 VERIFYING: T0/T1 tiers, the verification-kind DAG, d9's repair-loop budget -- an executed UoW lands in VERIFYING and SITS; nothing runs gates; no VERIFYING->RETRO or VERIFYING->EXECUTING call site exists anywhere. -> next - U3.7 RETRO/freeze (the instance is NOT frozen/pointed-at here) / U3.8 M1. -> later - Auto re-draft on amendments (Q-17-A stays open). -> later - Track B / P4 (the instance stays file-only; no WorkflowDag row exists to point at it). -> later - Content-aware DAG compilation (the V0 template consumed as-is; the generic frontier is what makes a richer compile worth building LATER). -> later ## (3) Verification gates G1 tests/test_orchestrator_executing.py 32/32 PASS live: the full happy-path arc (mock provider, injected I/O, zero network) on a toy workspace -- ratified skeleton -> executed DAG -> node kvs done -> evidence stamped -> state VERIFYING. G2 Frontier proven GENERIC: branching diamond respects dependency order (a first, d last, b/c between), multi-root join runs all roots, an `annotates` back-edge does not constrain (would be a cycle if it did), a `precedes` cycle refuses loudly, an unknown node kind refuses loudly. G3 Decline + failure paths resumable: declined/failed kv state visible IN THE INSTANCE FILE, engine state stays EXECUTING, and the follow-up run completes -- with the resume-after-decline leg proving ZERO provider calls (MustNotBeCalledAdapter) via egress-record recovery. G4 Q-15-B proven with real children: run_shell and run_gate children print ABSENT for a monkeypatched $ANTHROPIC_API_KEY; a non-secret var passes through; SSH_AUTH_SOCK survives the pattern. G5 Substrate round-trip: the written instance parses under the REAL dag/cppcc/bin/dagModel and re-renders bytes-identically (asserted via the "verified" outcome), droppings cleaned (steps/ holds exactly step.1.txt + step.1.execution.dag). G6 Live CLI smoke (not just tests): draft -> ratify --yes -> `b3ubot execute <ws> UT.1 --yes` in a scratch workspace + isolated $B3UBOT_HOME -> HUMAN GATE diff presented, "executed: UT.1 ... engine state VERIFYING", hello.py has the goodbye line, engine history [INTENT, SKELETON, RATIFIED, EXECUTING, VERIFYING]. Confirmed live before committing. G7 Full suite before this step's edits: 443 passed / 11 skipped (confirmed live, matching step.17's reported final). After: 479p/11s pre-step-file (+32 executing, +4 env-scrub); 481p/11s once this step.18.txt lands in the corpus (+2 self-inclusive pickups -- the steps 10-17 growth pattern). Zero regressions; zero existing tests edited. G8 Scope boundary held: no VERIFYING-tier code, no VERIFYING->RETRO or VERIFYING->EXECUTING call site, no d9 budget, no Track B row; this repo's own steps/ and dag/instances/ untouched by every test (asserted in-suite); this session stops after U3.5. G9 §11 ledger row updated THROUGH the existing `ledger` CLI (transition -> active --step step.18, close -> done/PENDING), against the REAL end_to_end.md -- dogfooding, the step.13-17 precedent. G10 b3ubot porcelain clean after each commit; retro pair via the SHARED v3_claude/scripts/step_gdiff (steps/step.18.gdiff.txt); exactly 2 commits (work + gdiff-with-hash-backfill-folded-in), the step.7-17 precedent. ## (4) LOCKs C-18-A b3ubot porcelain clean after each commit. C-18-B THE INSTANCE FILE IS THE LIVE PROGRESS SURFACE (§2.13): node kv state is rewritten through the trail-recorded tool surface on EVERY status change (running included), and the .dag TEXT is the single truth -- parser droppings are removed after every verification parse; no shadow state store may exist beside it. C-18-C A HALT IS RESUMABLE, NEVER A BUDGET: node failure and human decline leave state EXECUTING with the halting node's kvs persisted; re-run resumes (done skipped, failed/declined re-run). No retry counting, no cost ceiling -- d9 stays U3.6's open decision (the drafting one-retry precedent's spirit). C-18-D EVIDENCE IS ENGINE-STAMPED MECHANICAL FACT (C-16-B/C-17-B carried): the §(0) EXECUTION RECORD and every node fact kv are engine-written (timestamps, run ids, exit codes, applied paths) -- never provider prose; stamped ONCE on completion with the post-stamp re-parse guard. C-18-E THE GATE AND SCOPE NEVER WEAKEN INSIDE THE LOOP: the human gate runs BEFORE any write (the frontier's own precedes edge, and assume_yes is the only bypass -- C-17-E's injectable-I/O rule holds); C-4-B is enforced AT the apply, so approval never widens scope (../evil.txt-tested). C-18-F SECRETS NEVER REACH A CHILD (Q-15-B resolved): run_shell/ run_gate children get scrubbed_env() -- deny-by-pattern on secret-shaped names; any future child-process surface must route through _execute or match its posture (B-4). C-18-G NO SILENT SCOPE CREEP: U3.6-U3.8, Q-17-A, Track B and content-aware compilation named out of scope in (2) and confirmed not started (G8); the operator's explicit "stop after U3.5" direction is honored. C-18-H standing locks inherited: B-3 (no disclosure/no push); B-6 (deterministic pytest oracles gate this step); B-4 (served harder: the env scrub); B-5 (every provider_run egress-recorded unconditionally BEFORE the proposal is judged, C-4-C -- and the egress record is now also the resume source); C-15-B (every handler action, instance rewrite included, is on the tool trail). ## (5) Open questions Q-18-A Should a resumable halt also leave a marker in the step file's §(0) (a "halted at <node>" line), not only in the instance kvs + trails? LEAN: no -- §2.13 names the DAG the LIVE progress surface and the step file the ledger artifact; transient halt state in §(0) would pile up stamp blocks across retries and blur the one durable completion record. Revisit only if real operation shows humans reading the step file, not the instance, mid-UoW. Q-18-B Should the oracle_gate node name its script from the skeleton's own Verification gates text instead of the fixed scripts/oracle_gate.sh convention? LEAN: not yet -- the compiler's T3 detection is regex-V0 and no real skeleton names a T3 gate today (P5 hasn't landed); a naming scheme designed against zero real instances would be speculation. Revisit at U3.6/U5.2 when real gate scripts exist. Q-18-C Should provider_run send the whole ratified skeleton, or a §(2)-only slice (egress minimization)? LEAN: whole skeleton for V0 -- it is the ratified PLAN (the natural unit of execution context), it was PAL-drafted so its content already legitimately crossed the egress boundary once, and every send is C-4-C-recorded; slice design belongs to the P6/P7 context-budgeting work (§2.4's policy layer), not to an ad-hoc cut here. ## (6) Acceptance - [x] app/orchestrator/executing.py: legality-first execute_uow, compile -> write -> frontier loop -> kv persistence -> §(0) stamp -> VERIFYING (2.1, 2.9, G1, G6). - [x] Instance at steps/<step>.execution.dag beside the skeleton; full-rewrite persistence trail-recorded; droppings cleaned (2.2, 2.3, G5, C-18-B). - [x] Substrate round-trip verified through the real bin/dagModel both ways (available -> verified; absent -> named skip for writes, loud refusal for resume) (2.4, G5). - [x] Per-node-kind V0 handlers mapped onto what exists, provider_run egress-recorded unconditionally, tool_call C-4-B-enforced at the apply, oracle_gate exit-as-data (2.5, G1, C-18-D/E/H). - [x] Frontier generic over the DAG: diamond/multi-root/ annotates/cycle/unknown-kind all proven (2.6, G2). - [x] Halt-loudly-resumably failure semantics; egress-record proposal recovery on resume (zero provider calls, proven) (2.7, G3, C-18-C). - [x] Q-15-B resolved: scrubbed_env() in shell.py, real-child proofs for run_shell AND run_gate (2.8, G4, C-18-F). - [x] `b3ubot execute` CLI beside ask/draft/ratify: non-TTY refusal unless --yes; human-needs-TTY regardless (2.10, G6). - [x] 32 + 4 new tests, zero regressions, zero edited tests (443p/11s -> 479p/11s -> 481p/11s with this file) (2.11, G7). - [x] design.md §2.3/§2.7/§2.13 delivered-state notes; docs/tool_surface.md; docs/orchestrator_state_machine.md; every stale "U3.5 pending" docstring; end_to_end.md §5 row + §11 ledger via the `ledger` CLI (2.12, G9). - [x] Scope boundary held: U3.6-U3.8 + Q-17-A + Track B not started (G8, C-18-G) -- this session stops after U3.5. - [x] Retro pair: this file + step.18.diff.txt + step.18.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/executing.py + errors/__init__ + compiler ASCII fix + app/tools/shell.py env scrub + __init__/gitops + adapters/mock/fixtures.py + cli + ratify print + tests + design.md/end_to_end.md/docs + this file): ae78fe1 GDIFF commit (step.18.gdiff.txt via scripts/step_gdiff): (HEAD) End of step.