step.26.txt -- b3ubot U5.4a: the foreign-workspace arc -- b3ubot drives a COMPLETE UoW in a repo that is NOT itself (real git closure in the foreign repo; a [T3:validate] verdict from the plane inside the pass -- the pre-trinity rehearsal), plus THE ENGINE-STATE WORKSPACE GUARD: records stamped with their project, cross-workspace invocations refused loudly. The UoW row itself entered the ledger through the engine's own `ledger add-row` -- the amendment path, dogfooded. Step: 26 Parent: end_to_end.md §7 P5 -- U5.4a, ADDED BY AMENDMENT (the (p1) re-scope, step.51-of-b3u's header + this repo's memory NEXT-chain): everything before step.26 ran on toy/tmp workspaces or this repo's own ledger; (p2) is b3ubot managing ANOTHER project (the xml repo), so the foreign shape needs proving BEFORE U5.5 stakes the trinity gate on it. design §2.6 (the workspace manager: "clean tree per closed UoW"), §2.9 (deployment), §4 (the state machine whose records this step hardens). UoW: U5.4a -- fifth P5 UoW (by amendment); U5.5 (the trinity gate) is the last. Date: 2026-07-22 Origin: User directive 2026-07-22: "go with U5.4a, step file and its execution" (the (p1) sequence). ## (0) Status EXECUTED same-session. The guard: EngineStateRecord gains a `workspace` stamp (resolved root at start_state; None-tolerant for pre-step.26 records) and load_state gains `expect_workspace` -- threaded through all five stage entries (draft/ratify/execute/ verify/close), so a stage invoked from the WRONG project refuses loudly naming both roots and the per-project `$B3UBOT_HOME` remedy. The proof: a foreign git project (own files, own ledger, own history) driven through the complete arc with a [T0:py_compile] + [T3:validate] gate set -- closure landed TWO real commits in the FOREIGN repo (work + gdiff), tree clean, b3ubot's own tree untouched byte-for-byte; live twin took its T3 verdict from the launcher- booted b3u (p5_gates 14/14). The U5.4a ledger row was inserted by `b3ubot ledger add-row --after U5.4` -- the machinery placed it correctly past U5.4's note block (rehearsed on a copy first), and the table round-trips. Suite 632 passed + 25 skipped = 657 collected (measured after the last edit -- the step.25 rule, held; the first-draft claim of 658 was a hand-sum caught by the measure, same-day rule enforcement). ## (1) Ground truth probed at drafting (outputs -> named writers) CONFIRMED the M1 arc already closes in tmp GIT workspaces (tests/test_m1_arc.py: git-init fixture, MockAdapter, 5 stages to CLOSED with real commits) -- "foreign" in the tmp sense was NOT the gap. CONFIRMED the REAL gap: EngineStateRecord carried NO workspace field -- records keyed by UoW id alone in one shared data home. Project B invoking `verify U1.1` would silently read project A's record: the exact (p2) multi-project hazard (b3ubot's ledger + the xml project + any scratch coexisting under ~/.b3ubot). CONFIRMED the five stage entry points each do `record = load_state(uow, home)` with a workspace_root/root in scope (drafting also owns the start_state call) -- the guard threads through exactly five call sites; the in-function re-loads (ratify's post-decision reads) sit behind the entry check and need nothing. CONFIRMED `ledger add-row` takes --phase/--after and the U5.4 row now carries note lines -- placement after a noted row REHEARSED on a copy of end_to_end.md before touching the real one (the machinery handled it: inserted after the notes, before U5.5). ## (2) What U5.4a delivers 2.1 **The workspace stamp** (persistence.py): EngineStateRecord. workspace (resolved root, stamped at start_state; None for pre-step.26 records and workspace-less callers -- back-compat, the guard never guesses); from_dict tolerant. 2.2 **The cross-workspace guard**: load_state(..., expect_workspace=) -- a stamped record invoked from a different root raises OrchestratorError naming BOTH roots and the remedy (run from the recorded workspace; per-project $B3UBOT_HOME for same-named UoWs). Threaded through the five stage entries; drafting's start_state stamps. 2.3 **The foreign-arc proof** (tests/test_foreign_workspace.py, 7 hermetic): the complete arc in a foreign git project with T0+T3 gates (replay plane) -- CLOSED, exactly +2 commits in the foreign repo, evidence files there, tree clean, b3ubot's own porcelain byte-identical before/after; the T3 verdict row names the foreign UoW; the guard's refusal (both roots + the remedy in the message), the legacy-record pass, and the one-home-one-idspace start_state refusal. 2.4 **The live twin** (tests/test_foreign_workspace_live.py, 1): the same arc with the [T3:validate] verdict from the REAL launcher-booted b3u -- meta.sgr applied in the foreign repo, closure commits landed, verdict row recorded. The pre-trinity rehearsal, live. 2.5 **The amendment, dogfooded**: the U5.4a ledger row inserted via `b3ubot ledger add-row --uow U5.4a --phase 5 --after U5.4 --status active --step step.26` on the REAL end_to_end.md (rehearsed on a copy first); closure will use `ledger close` the same way. The §7 P5 phase table row added with the amendment provenance named. 2.6 **p5_gates.sh extended**: live 13 -> 14 legs; hermetic twin 56+13 -> 63+14; summary U5.1-U5.4a. 2.7 **Docs**: design §2.8-chain U5.4a note; tests/testing.md re-measured same-commit. EXPLICITLY OUT OF SCOPE (each a named later UoW, never silent): - the trinity gate itself (human adapter live leg; Q-25-A delivery placement) -> U5.5 - per-workspace data-home NAMESPACING (state under /orchestrator//...) -- the guard turns the hazard loud; namespacing is the richer future if (p2) ever needs same-named UoWs across projects under ONE home (the named remedy today: per-project $B3UBOT_HOME) -> later - stamping legacy records retroactively (they pass unguarded; rewriting history the engine did not witness would be a lie) -> never ## (3) Verification gates G1 THE GUARD: a stamped record + a wrong-root stage call refuses loudly with both roots and the $B3UBOT_HOME remedy in the message; an unstamped (legacy) record passes; a same-id second-project start_state refuses (one home = one id-space) -- hermetic green. G2 THE FOREIGN ARC (hermetic): complete arc in a foreign git repo, T0+T3 (replay) gates green, CLOSED, +2 commits THERE, evidence files THERE, foreign tree clean, b3ubot tree untouched byte-for-byte. G3 LIVE: the same foreign arc with the T3 verdict from the real plane -- p5_gates 14/14 live legs, hermetic twin 63 passed + 14 SKIPPED, B-4 grep clean. G4 THE AMENDMENT: the U5.4a row entered the real ledger via the engine's own add-row (placement past U5.4's note block correct), and the table suites stay green. G5 NO REGRESSION: full suite 632 passed + 25 skipped = 657 collected, 0 failed -- measured AFTER the last edit (the step.25 rule, held). G6 Hygiene: porcelain after the WORK commit; testing.md moved with the tests; ledger + design current; gdiff via ../scripts/step_gdiff. ## (4) LOCKs C-26-A b3ubot porcelain clean after each commit. C-26-B ONE HOME = ONE ID-SPACE: a UoW id names ONE project's work within a data home; the guard turns cross-project confusion into a loud refusal, and the multi-project answer is per-project $B3UBOT_HOME -- never a silent best-guess about which project a record "probably" belongs to. C-26-C THE GUARD NEVER GUESSES: unstamped records pass (the engine refuses to invent provenance it did not record); only a POSITIVE mismatch of two known roots refuses. C-26-D FOREIGN MEANS FOREIGN: the proof arc asserts b3ubot's own tree is byte-identical before/after -- managing another project must leave not one dropping here. C-26-E standing locks inherited: C-25-B/C/D (trail delivery, oracles-gate-arcs-assist, exit codes), C-24-B (client posture), C-23-B/C, B-1/B-3/B-4. ## (5) Open questions Q-26-A Guard placement (DECIDED: expect_workspace on load_state at the FIVE stage entries -- one guard at the resume primitive, visible in every stage's own code; a decorator/wrapper was REJECTED as hiding a refusal that should read inline; guarding advance_state too was REJECTED for V0: every advance follows an entry-guarded load in real flows, and double-guarding adds noise). Q-26-B Amendment mechanics (DECIDED: the engine's own `ledger add-row`, rehearsed on a copy first because U5.4's row carries note lines -- the machinery placed it correctly, which is itself U2.3-era code earning its keep on its own repo; hand-editing the table was REJECTED after step.25's Q-25-C burned exactly that path). Q-26-C Arc-vehicle for the proof (DECIDED: per-stage calls with a hand-placed skeleton + StaticProposalAdapter -- the T3-bearing gate set needs a skeleton the mock drafter does not script; run_uow_arc's draft stage is already M1-proven, so composing stages loses nothing and arranges everything). ## (6) Acceptance - [x] Workspace stamp + guard + five-entry threading (2.1, 2.2, G1); legacy pass + start_state refusal covered. - [x] Foreign-arc proof, hermetic 7/7 (2.3, G2). - [x] Live twin green; p5_gates 14/14; twin 63+14 (2.4, 2.6, G3). - [x] U5.4a row via the engine's own add-row; table suites green (2.5, G4). - [x] Full suite 632 passed + 25 skipped = 657 collected, 0 failed, measured after the last edit (G5). - [x] Docs moved in the same commit (2.7, G6). ## (7) Hash backfill WORK commit (everything above + this file): (this commit) GDIFF commit (step.26.gdiff.txt): (HEAD) End of step.