Orchestrata Ultimate › Plans › ast-daemon-deployment-plan

PLAN — the AST daemon as a real deployment

Source: plans/ast-daemon-deployment-plan.md at 8a2cb42ad · last changed 2026-10-07 · first committed 2026-10-05 · 1257 lines.

Prose pending.

Plan text

PLAN — the AST daemon as a real deployment

Status: RULED, ready to build (2026-10-05). D1 and D2 below were decided by Mohamed the same day (rulings R1/R2, recorded as ADR-0067); every packet is now engineering under the platform conventions. R2 puts the codemod daemon in the same wave, so each packet has an AST half and a codemod half.

0. Measured state (2026-10-05, this host)

Fact Evidence
orchestrata-ast-daemon.service in ~/.config/systemd/user is a plain unit whose WorkingDirectory and ExecStart name ~/workspace/orchestrata-ultimate-architecture and its .venv unit file; readlink /proc/<MainPID>/cwd
That checkout is on branch architecture/domain-model at d4b394279 (2026-09-23), dirty, and is not the primary checkout (~/workspace/orchestrata-ultimate, master) git -C … status -sb, log -1
The unit is not in the repo. The repo ships only the TEMPLATE deploy/systemd/orchestrata-ast-daemon@.service, started per-checkout and runtime-only by scripts/dev_daemons.sh. The "runtime-only, never ~/.config" rule is stated in that unit's header comment (labelled U-T2, citing ADR-0062); ADR-0062's own text does not contain it deploy/systemd/, scripts/dev_daemons.sh, grep -i runtime docs/adr/0062-*.md → no match
The unit survived today's prod promote and rollback untouched, because nothing installs or verifies it promote_env.py log: services stage never names it
It was inactive after the promote (cause not established) and was restarted by hand systemctl --user status, 21:52
Each session's start-ast-daemon.sh hook ALSO spawns a daemon per checkout from that checkout's venv, detached with setsid, socket <repo>/.claude/run/ast-daemon.sock .claude/hooks/start-ast-daemon.sh
The daemon is pure acceleration: serialize_codebase_ast.py falls back to a local computation when the socket is absent or unhealthy scripts/ast_daemon.py docstring; query_ast_daemon
Footprint of the running instance RSS 546 MB at 4 min uptime (this session); 1.7 GB RSS recorded 2026-08-30 in the template unit's header comment
/opt/orchestrata-tools already deploys scripts/ wholesale into lib/scripts, has its own 3.14t venv, records VERSION, and ships ast_live_sync as a project-agnostic tool with --repo-root deploy/install-tools.sh
The daemon's deps beyond stdlib: fastapi, uvicorn, pydantic, watchfiles (tools venv has watchfiles, neo4j only) imports in scripts/ast_daemon.py; install-tools.sh
ast_daemon.py has --socket, --source, --cache, idle/watch knobs, no --repo-root; it resolves ast_api.py beside itself and the source dir relative to cwd argparse block; _HERE

Three distinct violations of the platform conventions:

  1. Deploy-in-place. The running daemon executes a working tree's code and venv (ExecStart=…/workspace/…/.venv/bin/python scripts/ast_daemon.py).
  2. No identity. Nothing records which commit it runs, no installer resets it to a pinned ref, and no verifier refuses success when the running thing is not the deployed thing. It went inactive and nobody was told.
  3. Config carries a checkout path. The unit is persistent, version-less wiring that names a developer checkout — the exact thing U-T2 removed from the repo, surviving on this host outside the repo.

1. Target

The AST daemon is code deployed once, serving N checkouts:

2. Decisions — RULED (Mohamed, 2026-10-05, ADR-0067)

D1 — may an enabled instance persist the served checkout path? Asked because systemctl --user enable orchestrata-ast-daemon@<escaped-path> writes a symlink naming the checkout, and U-T2 had made dev instances runtime-only on the ground that a checkout path in config was deploy-in-place.

R1 (Mohamed): "every deploy must follow the professional deployment methods and be separate from code." Applied: the daemon's code lives only under /opt/orchestrata-tools; the served checkout is a runtime parameter (%I, --repo-root), i.e. data. An enabled instance therefore names data, not code, and option (a) stands: enable for the primary checkout only; worktrees stay runtime-only via the hook. R1 also removes the checkout-venv fallback I had kept in P4 — a process executing from a working tree is the defect, whatever starts it; without the deployment the client takes its slow path. Recorded as ADR-0067 (ADR-0062 is still Proposed and never carried the runtime-only clause, so a new record, not an amendment).

D2 — codemod daemon in the same wave? R2 (Mohamed): yes. orchestrata-codemod-daemon.service has the identical defect (plain unit, checkout-bound; measured: -m orchestrata_ultimate.refactor.codemod._daemon_entry <repo> <repo>/.codemod_daemon/socket <repo>/.codemod_daemon/pid from the architecture checkout's venv, cwd there, RSS 1.9 GB at 5 h 40 min, enabled). Each packet below carries a codemod half; the AST half lands first within the packet so the shape is proven before it is copied.

Codemod facts the halves rely on (measured 2026-10-05): the entry already takes repo_root socket pid_file positionally, so it is location-independent except for its imports; it imports codemod_fast (native, built from scripts/_codemod_fast/, with a pure-Python fallback the tests cover), psutil and xxhash, none of which the tools venv has; the client finds the socket at <repo>/.codemod_daemon/socket; CodemodDaemon.start(repo_root) spawns the entry with sys.executable; no session hook starts it today; scripts/dev_daemons.sh manages both templates together.

3. Packets

Each packet: one worktree off origin/master, one brief, gates green, ship. Opus decides, Sonnet builds. Numbers in acceptance criteria are to be MEASURED and written into the commit message. Each packet has an AST half (listed first) and a codemod half (↳ codemod:), shipped together unless the codemod half is blocked, in which case the AST half ships and the blocker is reported — never silently dropped.

Packet Definition of Done (ADR-0069 / RUL-034, Mohamed 2026-10-06, applies to P4–P7 from this date; P1–P3 shipped before the ruling and owe the gap in P7). Five items in the same ship as the code: (1) @traces on every added/changed symbol and test, spec-defined ids only; (2) RTM regenerated (generate_rtm_evidence.py, trace_intel.py untraced-reqs before/after); (3) checklist/docs//design-status updated in the code commit; (4) the knowledge-wiki page for the daemons updated and shipped; (5) ≥90% line coverage per touched source file, measured. The ship log gains the five as columns from P4 on.

P1 — make the daemon location-independent (code)

scripts/ast_daemon.py, scripts/ast_api.py

P2 — deploy the daemon with the tools (installer)

deploy/install-tools.sh

P3 — the unit executes deployed code only (unit)

deploy/systemd/orchestrata-ast-daemon@.service

P4 — hook and client use the deployed daemon only, verify identity

.claude/hooks/start-ast-daemon.sh (+ claude/hooks/ mirror via gen_plugin), scripts/serialize_codebase_ast.py

P4 pre-build record (2026-10-06, measured in wt-AST-DAEMON-P4 at d46b99595):

P4 build record (2026-10-06, builder commits 3bc89cca5 + 2ae7a8090):

P4 review round 1 (2026-10-06, reviewer on 3bc89cca5+2ae7a8090): HOLD.

P4 fix round 2 (2026-10-06, builder, commit 46e2d0a4f): both blockers, minors 3-8 and both pre-existing test defects addressed; awaiting reviewer re-check. Builder-reported evidence (orchestrator has not yet re-run it):

P4 review round 2 (2026-10-06, reviewer on 46e2d0a4f): HOLD. Both round-1 blockers reproduced closed: the ordered pair test_ast_daemon.py::test_hook_source_has_no_checkout_spawn + tests/claude/test_start_ast_daemon_hook.py in ONE invocation → 12 passed in 79.15s, rc=0, pytest not signalled (hook refuse branch at start-ast-daemon.sh:102-107 covers $$, $PPID, cmdline-without-ast_daemon; self-pid and bystander tests drive a real ThreadingUnixStreamServer /health); check_external_tools.py → 8 declared, systemctl/systemd-escape OK 257.5 ≥ 250.0, PASS. Minors 3, 4, 5, 6 (loaded-unit half), 8 proven by execution; minor 7's CalledProcessError branch proven by reading only. Test defects fixed; shutdown_timeout default == old constant 5.0. Gates: bash -n OK both hook copies (byte-identical), gen_plugin.py --check current, ruff/black clean on the 6 .py files, no attribution, no leftover p4-* units or scratch dirs. Six modules on the committed tree: hook 11 passed (126 s); lifecycle 45 passed; coverage 5 passed; codemod CLI 43 passed; gen_plugin 35 passed; tests/scripts/test_ast_daemon.py RED in 2 of 2 runs (run 1: 2 failed, 41 passed, 1 error, load 38.9; run 2: 1 failed, 42 passed) — the builder's "43 passed" did not reproduce. HOLD reasons, all owned by fix round 3:

P4 fix round 3 (2026-10-06, on top of 13e551b46). DESIGN_WAIVER: ADR-0067.

P4 fix round 4 (2026-10-06, on top of ffadad98c). DESIGN_WAIVER: ADR-0067. Clears the round-3 reviewer HOLD items that need no ruling.

  1. RTM blocker (d4fee128f): reports/rtm-evidence.{json,md} regenerated with scripts/generate_rtm_evidence.py --verify --verify-timeout 600 (178 citing modules, 33 min). Header: "Execution evidence this run: YES (--verify)". Totals: 7262 requirements, TESTED=333, FAILING=11, DECLARED=2974, UNLINKED=3944, FIXTURE=5, GHOST=4, MALFORMED=6. Eight modules graded RED in that run, none in the P4 diff: tests/conftest.py, tests/e2e/test_full_pipeline.py, tests/knowledge_graph/test_context_pack_performance.py, tests/orchestrator/test_pipeline_e2e_{analysis,docs_session,review}.py, tests/refactor/codemod/test_workspace_load.py, tests/scripts/test_trace_intel.py; not investigated this round.
  2. TOCTOU (dccff2c00): CodemodDaemon.start() acquires the lock first and runs _refuse_if_running plus the launcher check inside the locked region; no systemctl start precedes either. Tests: test_start_refuses_only_under_the_lock (lock held by the test: start fails on the lock and the stale PID file the refusal would remove is still there) and test_concurrent_starts_exactly_one_proceeds (two threads, barrier, real unit: exactly one starts).
  3. Repo-tied predicate (dccff2c00): _pid_is_codemod_daemon(pid, repo_root) requires argv -m orchestrata_ultimate.refactor.codemod._daemon_entry <repo_root> with the resolved repo root as the element right after the module (the launcher codemod-daemon <repo_root> <socket> <pid_file> and the unit's ExecStart ... /%I ... both pass it first). _refuse_if_running and stop() pass the repo root. TestPidIsCodemodDaemon gains the different-repo-root case and a -m <module> with no root case.
  4. Tests (dccff2c00): hook MainPID branch — test_hook_accepts_the_units_main_pid_without_an_ast_daemon_argv and test_hook_refuses_a_pid_that_is_neither_the_main_pid_nor_an_ast_daemon against a real running user unit whose launcher is rewritten to a non-ast_daemon.py argv; lifecycle — test_the_real_units_main_pid_is_our_daemon_and_this_process_is_not against a real started unit. The hook body is unchanged (no defect exposed). Also test_zombie_child_counts_as_dead got a 60 s bound (was 5 s): it failed at load average ~35 on interpreter start-up alone, with the assertion unchanged.
  5. Wiki (knowledge-wiki a84c634): claude/hooks/start-ast-daemon.md rewritten for the systemd user-unit hook, MainPID identity, argv matching, $$/$PPID refusal, no checkout spawn; the code block equals the committed hook byte for byte. Not pushed. Measured: daemon.py coverage 91% (268 stmts, 23 missed) over test_daemon_lifecycle.py + test_daemon_coverage.py (61 passed); hook module 14 passed. REQ-110-135's text amendment is ADR-0071 (Proposed, awaiting Mohamed); specs/110-codemod-engine/spec.md and its checklist are untouched.

P4 review round 4 (2026-10-06): GO. Reviewer re-measured 61 + 14 passed, daemon.py 91%, ruff/black clean, specs diff empty, wiki code block identical to the hook. The 8 RED RTM modules are none of them P4-caused: 5 are FAILING on master's RTM too (test_full_pipeline, the three test_pipeline_e2e_*, test_context_pack_performance), tests/conftest.py is a harness artefact (run_citing_tests runs pytest tests/conftest.py, exit 5 = nothing collected, graded RED), test_workspace_load.py is load-only (passes at idle; REQ-110-001..004 read FAILING in the committed RTM for that reason), test_trace_intel.py is a worktree-environment gap (.cache/trace_intel/codebase_facts.pl has 0 runtime_call( facts here, 7628 in the main checkout; cites only FIXTURE ids). Three MINOR findings carried to P5 as debt, none blocking:

P5 — retire the checkout-bound units and gate their return

P5 build record (2026-10-06, builder commit 240053176 on feat/ast-daemon-p5-retire-units, DESIGN_WAIVER ADR-0067 R1/R2).

Host before-measurement (builder session, before any host action): orchestrata-ast-daemon.service and orchestrata-codemod-daemon.service both enabled / inactive under ~/.config/systemd/user; ss -xp showed no client on any .codemod_daemon/socket. Real-host python scripts/check_prod_tree_separation.py (default unit dir), exit 1 — 20 units examined, 4 offending directives in the 2 plain units, none yet retired (the ship retires them, never a hand step):

[prod-separation] FAIL — RUL-027: 20 unit(s) examined, 4 directive(s) point production at a DEVELOPMENT checkout (--dev-tree <this worktree>)
  orchestrata-ast-daemon.service:15: WorkingDirectory=/home/mohamed/workspace/orchestrata-ultimate-architecture  [under a /home/*/workspace/ checkout (non-template unit)]
  orchestrata-ast-daemon.service:17: ExecStart=/home/mohamed/workspace/orchestrata-ultimate-architecture/.venv/bin/python \  [under a /home/*/workspace/ checkout (non-template unit)]
  orchestrata-codemod-daemon.service:14: WorkingDirectory=/home/mohamed/workspace/orchestrata-ultimate-architecture  [under a /home/*/workspace/ checkout (non-template unit)]
  orchestrata-codemod-daemon.service:16: ExecStart=/home/mohamed/workspace/orchestrata-ultimate-architecture/.venv/bin/python \  [under a /home/*/workspace/ checkout (non-template unit)]

What shipped:

Tests shown to fail on the pre-fix code (no git stash): (3) git checkout afd1e82ae -- daemon.py (pre-fix predicate) then the class: 2 failed (the relative-root test and the unreadable-cwd test), 5 passed; restored with git checkout HEAD --. (2) release removed from the finally (pass): both already running tests failed (Lock acquisition failed: [Errno 11]); restored. (4) a 400-spawn loop under 16 busy threads: the old no-wait shape read the real daemon-shaped child as not-a-daemon 36 / 400 times, the _await_argv shape 0 / 400. (1) is a rename, not a strengthened assertion.

Measured: 242 passed, 0 skipped in one run over test_check_prod_tree_separation.py (42 collected), _paths, test_daemon_lifecycle.py (58) + test_daemon_coverage.py (5) and tests/promotion/test_identity_*.py; 33 passed, 0 skipped over tests/deploy/test_install_tools.py + test_ast_daemon_unit.py + test_retire_plain_units.py. Coverage from the first run: scripts/check_prod_tree_separation.py 96 % (89 stmts, 4 missed: 110-111, 154-155 — the OSError guards and the no-unit-dir early return), refactor/codemod/daemon.py 91 % (269 stmts, 23 missed, all pre-P5 branches). RTM: no @traces set changed (_pid_is_codemod_daemon keeps REQ-110-135; the new tests carry @traces(spec="110", reqs=["REQ-110-135"]) on the same id), so the committed RTM is left alone.

Ship acts (not done by the builder): the P5 ship's installer run performs the retirement. Expected after it: systemctl --user cat fails for both plain units, ~/.config/systemd/user/orchestrata-{ast,codemod}-daemon.service absent, and the real-host check_prod_tree_separation.py exits 0.

Review round 1 (NO-GO) → fix round 1 (2026-10-06, DESIGN_WAIVER ADR-0067: every item hardens P5 deliverables (b)/(c) or closes a P4 MINOR more precisely).

Measured (fix round 1): 70 passed over test_check_prod_tree_separation.py + _paths; 64 passed over test_daemon_lifecycle.py (59) + test_daemon_coverage.py (5); 36 passed over test_install_tools.py + test_ast_daemon_unit.py + test_retire_plain_units.py (12); 254 passed over tests/promotion/; 424 passed, 0 skipped in total. Coverage (superseding the first-run figures above): scripts/check_prod_tree_separation.py 97 % (119 stmts, 4 missed: 166-167, 210-211 — both pairs are OSError guards: the _is_under_root resolve() guard and the unit read_text guard; the no-unit-dir early return is covered), refactor/codemod/daemon.py 92 % (274 stmts, 23 missed, all pre-P5 branches). Real-host scripts/check_prod_tree_separation.py is still exit 1, same 4 directives (before-state, retired by the ship); the two ExecStart lines now print as one logical line (the \ continuations joined) at their first physical line, 17 and 16.

Review round 2 (GO with MINORs) → fix round 2 (2026-10-06, DESIGN_WAIVER ADR-0067: every item hardens deliverable (b) gate / (c) retire-verify or fixes a test race; no new design decision).

Measured (fix round 2): 102 passed over test_check_prod_tree_separation.py + _paths + test_install_tools.py + test_ast_daemon_unit.py; 16 passed over test_retire_plain_units.py (two runs, 6 + 10); 64 passed over test_daemon_lifecycle.py

Process slip (fix round 2, recorded by the orchestrator): after the code commit the builder ran git checkout HEAD -- plans/ as hook-litter cleanup, which reverted its uncommitted plan edits for this record; they were re-applied and committed as 21a806cbd. No code was affected. The litter command names the six generated files explicitly for this reason — never a directory.

Review round 3 (2026-10-06, against a2e6b3913 / 21a806cbd): GO. All nine round-2 items confirmed closed; probes matched systemd for three/four trailing backslashes, blank line after a continuation, EOF mid-continuation, comment lines inside and outside a continuation; home seam evaluated at call time, gates.py unaffected; fixture end-to-end retire → verify → retire → verify exit codes 0/0/0/1; simulated post-retire host unit dir: gate exit 0 (18 units) with the default and the main-checkout --dev-tree. 436 passed / 0 skipped across five groups. Two NOTEs: this slip note was missing (fixed above); the ship-log row listed only the first commit (fixed at ship).

P6 — enable the primary instances and prove identity end to end

P6a — the prod identity verifier judges tool-tree template instances against their own root (first item, 2026-10-07)

Measured on the S3 and PH1 ships (s3_ship2.log, ph1_ship2.log): check_identity(root=/opt/orchestrata, …) discovers orchestrata-ast-daemon@home-mohamed-workspace-…wt-SPEC-110.service through SERVICE_GLOBS and refuses on six lines: c0 "resolves outside ROOT and system prefixes" for the three %I-derived tokens (--repo-root /<repo>, --socket /<repo>/.claude/run/ast-daemon.sock, ExecStartPre=/usr/bin/test -e /<repo>/.git), /proc/<pid>/cwd expected /opt/orchestrata got /opt/orchestrata-tools, cmdline[0] expected under /opt/orchestrata got /opt/orchestrata-tools/.venv/bin/python, and start-time measured against /opt/orchestrata/VERSION. Every one of those judgements applies the product root to a unit ADR-0067 R1 defines otherwise: the instance parameter is DATA ("an enabled instance names data, not code") and the code root is /opt/orchestrata-tools (P3: the unit executes deployed code only). argv0 already passes because P3 put /opt/orchestrata-tools in SYSTEM_PREFIXES.

Executed design (no new decision; each rule is R1 or P3 applied):

  1. Per-unit root. A discovered unit whose name carries an instance (@ before .service) AND whose ExecStart argv0 resolves under /opt/orchestrata-tools is judged against that root: root_resolved, venv_python (<root>/.venv/bin/python) and version (<root>/VERSION, same commit/deployed format, parsed with parse_version) are taken from the tools tree for checks c0, d/e (check_process) and the start-time floor. A template instance whose argv0 resolves elsewhere stays judged against the product root (a checkout-bound instance is still the defect).
  2. Instance parameter is data. For such a unit, c0 tokens equal to the unescaped instance (systemd-escape --unescape --path <instance>, read from the unit name; the /%I form is an absolute path) or under it are exempt. Nothing else is exempt: a token outside the instance, the tools root and the system prefixes is still a mismatch.
  3. Report names the root. Each such unit's UnitFacts carries root and the rendered report prints judged against /opt/orchestrata-tools (ADR-0067 R1: instance parameter is data) once per unit, so a reader sees which record each verdict used.
  4. A tools deploy restarts live instances. Measured 2026-10-07: the wt-SPEC-110 instance started 02:24 EDT and /opt/orchestrata-tools/VERSION was re-stamped at 05:07 EDT by the PH1 ship, so under rule 1 the instance is stale code (start-time below the tools deployed), the identity defect the verifier exists for. deploy/install-tools.sh gains, after its verification section succeeds and VERSION is promoted, a restart of every ACTIVE orchestrata-ast-daemon@*.service and orchestrata-codemod-daemon@*.service instance in the user scope (systemctl --user restart), logged per instance; inactive instances are left alone (worktree instances are runtime-only, P4). The P4 hook already replaces a stale daemon at session start; this closes the window between a deploy and the next session. Refinement after the first build (695d41954, adc7dbe0d): the block as first written ran only on the default prefix and so had no behavioural test, because install-tools.sh has no systemctl seam and its tests deploy to a tmp prefix, where restarting the user's live instances would be wrong. Orchestrator's pick (reversible, mirrors retire-plain-units.sh): the block moves to deploy/restart-tool-instances.sh, which takes the unit globs and the systemctl command from env (TOOL_INSTANCE_GLOBS, SYSTEMCTL), so a test drives it with a recording systemctl stand-in script and transient units it creates itself, and install-tools.sh calls it on the default-prefix path only. The restart arm then has a positive test: an active instance is restarted, an inactive one is not, a restart failure refuses with the "VERSION promoted; … did not come back" message.
  5. Tests: tests/promotion/test_identity_tool_tree_instances.py (new; real transient template-shaped units through _identity_fixtures._transient_unit with a tools-shaped root under tmp_path carrying VERSION and .venv/bin/python): a template instance whose argv0 is under the tools root passes with instance-parameter tokens present and is reported as judged against that root; the same unit with a token outside both roots still fails; a template instance whose argv0 is under a workspace checkout still fails against the product root; a plain (non-template) unit is unchanged; start-time is compared with the tools VERSION. tests/deploy/test_install_tools.py: the restart block restarts only active instances and only the two template names (dry-run/SYSTEMCTL seam as the script's other tests use). Coverage ≥ 90% on identity.py, identity_checks.py, identity_scope.py.
  6. Acceptance: promote --to prod verify no longer lists the AST-daemon instance; the only remaining refusals are R16b (ADR-0072) and the backup oneshot until its promote.

P7 — docs

PH1 — prod unit health (the two failed timer-driven oneshots)

Named after the second ship (2026-10-06); diagnosed 2026-10-07 after the S3 ship's prod promote ran to verify and refused on, among other things, orchestrata-kg-retention.service and orchgraph-backup.service in failed.

3b. Ship log

Packet Shipped master Verify Deploy Promote prod
P1 (both halves, 4 commits + evidence) 2026-10-06 e66a93b7b 410 passed / 0 failed, 18 modules, 138 s Claude config, tools (VERSION e66a93b7), gitlab-runner verified REFUSED — no GREEN test-env record for the new master.
plan + ADR-0067 (7 commits + evidence) 2026-10-06 9079327bd 122 reused green / 0 failed all three verified at 9079327b FAILED at prod verify, rolled back to 287826a9: the identity verifier inspects every orchestrata-* user unit and found orchestrata-ast-daemon.service (the ad-hoc plain unit) with no process AND seven path-directive violations (WorkingDirectory/ExecStart under a workspace checkout).
P2 (both halves, 3 commits + evidence) 2026-10-06 a1dd0e866 350 passed / 0 failed, 33 modules, 74 s Claude config verified; tools REINSTALLED and verified (VERSION a1dd0e866, both daemon smokes OK) — then install-tools.sh exited 1 in its EXIT trap (root-owned build artefacts, see the P2 build record), so gitlab-runner deploy did not run not reached (ship stopped at deploy). Re-shipped with the trap fix — see the next row.
P2 trap fix (1 commit + evidence) 2026-10-06 4c126dcc3 338 passed / 0 failed, 1 module, 41 s Claude config, tools (VERSION 4c126dcc3, both daemon smokes OK, trap exited 0 — no root-owned stage left behind), gitlab-runner all verified at 4c126dcc3 REFUSED — no GREEN test-env record (same standing refusal as P1; the P5 identity blocker sits behind it).
P2 socket cleanup (1 commit + evidence) 2026-10-06 5288f630b 339 passed / 0 failed, 1 module, 67 s Claude config, tools (VERSION 5288f630b, both daemon smokes OK, no p2-* socket litter left under /tmp), gitlab-runner all verified at 5288f630b REFUSED — no GREEN test-env record (same standing refusal; P5 unblocks).
P3 (first attempt, 9c1cd1729) 2026-10-06 — FAIL before the suite: test-purity gate, lexical "stub" in the new unit test (reports/run-tests-latest.json = aborted: gates) nothing deployed not reached
P3 units + installer (2 commits + evidence) 2026-10-06 83c604886 386 passed / 0 failed, 35 modules, 160 s Claude config, tools (VERSION 83c604886; both templates installed into ~/.config/systemd/user, byte-identical to staged, systemd-analyze --user verify OK on the rendered worktree instance), gitlab-runner all verified at 83c604886 REFUSED — no GREEN test-env record (same standing refusal; P5 unblocks).
P5 (brief afd1e82ae; build 240053176 + 49cb6c600; fix round 1 568fc1813 + 872ce77d8; fix round 2 a2e6b3913 + 21a806cbd; round-3 record f4a17f6c1 + ship evidence) 2026-10-06 05d92dfc4 (9 commits fast-forwarded from 2d64c35c9) 633 passed / 0 failed / 0 skipped, 39 modules, 166 s Claude config (VERSION 05d92dfc, 143 files), tools (VERSION 05d92dfc4; codemod_fast + ctok rebuilt; templates byte-identical; retire step ran: both plain units disabled, stopped, files and default.target.wants links removed, daemon-reload; verify arm: both not installed), gitlab-runner — all verified at 05d92dfc4. Post-ship real-host check_prod_tree_separation.py: OK, 18 units, exit 0 (first green since the gate exists). REFUSED — identity blocker GONE; the remaining refusal is "no GREEN test-env suite run on record (promote_env.py test test)"; the ledger had no records at all. promote_env.py test test run 1 (2026-10-06 18:17, this worktree at 05d92dfc4, box contended — 1930 s against a normal ~480 s): 38020 passed / 4 failed / 4 errors in 4 of 1879 modules, record FAIL. Solo re-run of the four at idle: test_test_executor.py, test_session_meta.py, test_check_feature_loss.py green (load/timeout flakes); tests/scripts/test_verify_package.py::test_full_pytest_target_pass_and_fail stays red at 80 columns and passes at 250 → real defect: scripts/verify_package.py::_run_one_full_target captured the child pytest's -rf summary, which pytest truncates to the terminal width (80 with no tty), so the assertion message never reached detail. Fixed by pinning COLUMNS=400 in the child env; module 20/20 at 80 columns after the fix. Run 2 recorded in the next row.
P5 verify_package fix + record (cb04d5425 + ship evidence) 2026-10-06 282724f59 (2 commits fast-forwarded from 05d92dfc4) promote_env.py test test run 2 at idle: 38028 passed / 0 failed / 0 skipped, PASS test test — the ledger's first GREEN test record (test=PASS@2026-10-06T23:06:01Z); ship verify reused it Claude config (VERSION 282724f5), tools (VERSION 282724f59; retire step: both plain units already gone, verify arm not installed ×2), gitlab-runner — all verified at 282724f59 Promotion ran for the first time (validate prod PASS; tree 287826a9 → 282724f59; deps unchanged; assets; VERSION; 4 services restarted) and FAILED at verify, rolled back to 287826a9 (rollback restart of the consumer also failed — same cause as below, so prod is where it was before the ship). Three independent findings from the identity verifier: (1) ADR-0066 R16b — orchestrata-conversation-consumer.service exits 1: CONVERSATION_KG_URI/CONVERSATION_KG_DATABASE absent from /opt/orchestrata/.env (5 keys, no conversation keys); refused by design until Mohamed names the endpoint → ADR-0072 (Proposed). (2) Verifier scope gap (ADR-0067 R1, P6 scope): the P4 template instance orchestrata-ast-daemon@<wt-SPEC-110>.service (started by the hook) is judged as an /opt/orchestrata unit — its %I checkout paths "resolve outside ROOT", cwd/cmdline[0] are /opt/orchestrata-tools (correct for a tool-tree unit), start-time predates the promote (it is not a prod service and is not restarted by promote). The verifier must classify orchestrata-*-daemon@*.service instances as /opt/orchestrata-tools identities with %I args as data. (3) Prod unit health, pre-existing (boot health already listed 4 failed user units): orchestrata-kg-retention.service failed — its unit lacks Environment=ORCHESTRATA_ENV_PROFILE=prod + EnvironmentFile=-/opt/orchestrata/env/prod.env (which orchgraph-backup.service has), so orchestrata-kg-ready waited on the TEST graph bolt://10.53.2.10:7687; orchgraph-backup.service exits 1 after "snapshotting database 'orchkg'" with no stderr in the journal or in /var/lib/orchestrata/backups/orchgraph/backup.log (diagnosis owed). Neither is P5's; both block prod verify (ActiveState: expected != failed).
P5 ADR-0072 + promotion record (docs/ADR/README/plan rows + ship evidence) 2026-10-06 43f0dc043 (2 commits fast-forwarded from 282724f59) ship verify: affected suite 122 passed / 0 failed / 0 skipped (--affected, non-authoritative subset; the authoritative record is the GREEN test=PASS@2026-10-06T23:06:01Z ledger entry from the row above); ruff, black, RUL-007, Prolog boundary, gen_plugin --check clean Claude config (VERSION 43f0dc04), tools (VERSION 43f0dc043; both daemons exit on SIGTERM; retire verify arm not installed ×2), gitlab-runner — all verified at 43f0dc043 Promotion ran a second time (validate prod PASS; tree 287826a9 → 43f0dc043; VERSION recorded; 4 services restarted: ast-live-sync, conversation-consumer, gateway, kernel) and FAILED at verify, rolled back to 287826a9 on the same three non-P5 findings as the row above, now in this order: (2) the P4 template instance orchestrata-ast-daemon@…wt-SPEC-110.service "ExecStart resolves outside ROOT and system prefixes" (P6 verifier item); (3) orchestrata-kg-retention.service and orchgraph-backup.service ActiveState: expected != failed (PH1). The consumer is absent from this run's verify output only because of timing: verify sampled it at 19:19:0x EDT while the freshly restarted process was still up, and it exited 1 at 19:19:17 EDT with the same RuntimeError: CONVERSATION_KG_URI and CONVERSATION_KG_DATABASE not set; the rollback restart at 19:19:19 failed identically and the unit is failed (NRestarts=1). Its R16b refusal is unchanged (ADR-0072 Proposed, awaiting Mohamed). Prod stays at 287826a9. Log: session scratchpad p5_ship3.log.
PH1 prod unit health (plan + brief 2cfa44294; fix ca8a65965; plugin regen 38ee9ba14; rode with the S3 ship-log row e50693b9e) 2026-10-07 b3a3c26cc (5 commits fast-forwarded from 4fadfaac1; ship attempt 1 refused at the gen_plugin --check gate because the plugin's reference copy of the retention unit had not been regenerated, fixed in 38ee9ba14) affected suite 147 passed / 0 failed / 0 skipped (tests/deploy/test_deploy_assets.py, new test_a_kg_gated_unit_declares_the_prod_profile red on the old template, green on the new); ruff, black, RUL-007, rulings-lint clean Claude config, tools (VERSION b3a3c26c), gitlab-runner verified RAN TO VERIFY AND FAILED, rolled back to 287826a9: units installed from b3a3c26cc (the corrected retention unit is now the installed definition), VERSION recorded, 4 services restarted, then verify refused on the orchestrata-ast-daemon@…wt-SPEC-110 template instance (P6 first item: judged as an /opt/orchestrata unit although ADR-0067 R1 makes its instance path data and its code root /opt/orchestrata-tools), on the two oneshots' stale failed state, and the rollback restart of the consumer failed on R16b as before. PH1 proven afterwards by hand: systemctl --user start orchestrata-kg-retention.service with the installed unit resolved neo4j://10.53.1.160:7691 on attempt 1, swept 0 expired GatewayAuditEvent / Session / CXPPacket nodes, Result=success, so the retention unit no longer counts against the verify. orchgraph-backup.service stays failed until a promote deploys master's scripts/backup_orchgraph.sh (8b2038b92) to /opt/orchestrata; the deployed 287826a9 copy exits 1 in its container lookup. Log: session scratchpad ph1_ship2.log.
P6a identity verifier judges tool-tree instances against their root (plan+brief 7538f2916, d5da265eb; code 695d41954, adc7dbe0d, 6955ed40c; manifest edge for the new script; PH1 row a3728f5a0) 2026-10-07 1fed577b4 (8 commits fast-forwarded from b3a3c26cc; ship attempt 1 refused by --affected: deploy/restart-tool-instances.sh resolved as untested because the stem convention keeps hyphens while the module is test_restart_tool_instances.py; declared in the manifest with a proof test) affected suite 763 passed / 0 failed / 0 skipped, 29 modules, 130 s; identity modules 142, deploy 66 + 113; coverage identity.py 94% / identity_checks.py 100% / identity_scope.py 97%; ruff, black, pyright, RUL-007, rulings-lint, gen_plugin --check clean Claude config, tools (VERSION 1fed577b; the new restart arm restarted the live orchestrata-ast-daemon@…wt-SPEC-110 instance: "code under /opt/orchestrata-tools changed beneath it"), gitlab-runner verified FIRST GREEN PROD PROMOTE. promote --to prod: tree 287826a9 -> 1fed577b4 (reset --hard), uv.lock unchanged, units installed, VERSION recorded 1fed577b439c (closes backlog (14) VERSION drift), 4 services restarted, verify OK "4 unit(s) active; 1 endpoint(s) listening; identity OK 1fed577b439c (11 unit(s))", both graphs bootstrapped (839 s). Prod tree is at 1fed577b4 with the fixed backup script and the corrected retention unit; orchgraph-backup.service left failed for inactive with the reinstall and runs next at 03:00. Ship itself then exited 1 on its OWN post-check 5a: PROD_ROOT defaulted to the pre-ADR-0062 $HOME/prod/orchestrata-ultimate, read "prod is at none"; never seen before because every earlier promote failed upstream. Fixed in 08c2e6d9a (default /opt/orchestrata; 5b gains the verifier's not-runnable skip so orchestrata-bot does not fail it). Finding F-P6a-1 (promoter verify, not fixed here): orchestrata-conversation-consumer.service started 05:52:46, exited 1 at 05:52:47 on the ADR-0072 CONVERSATION_KG_URI RuntimeError and crash-looped (RestartSec 10); the promoter's verify at 8.6 s sampled it inside that window and counted it active, so a crash-looping unit passed. The verify needs a settle window at least RestartSec long plus NRestarts == 0 and ActiveState=active at the end of it (NRestarts is already in UNIT_PROPERTIES). Ship's 5b, with the corrected root, will refuse on the consumer until ADR-0072 is ruled, which is the honest state. Log: p6a_ship2.log.
P6a ship-check fix 5a/5b (08c2e6d9a + evidence) 2026-10-07 ab189ccc7 (2 commits fast-forwarded from 1fed577b4) ship modules 103 passed / 0 failed (hand run); ship --affected 83 passed / 0 failed, 1 module; gates clean Claude config, tools (VERSION ab189ccc7), gitlab-runner verified Promote green again: tree 1fed577b4 -> ab189ccc7, verify "4 unit(s) active … identity OK ab189ccc722f (11 unit(s))", both graphs bootstrapped (570 embedding calls, 5,740 chunks). Ship rc 0, but 5b was wrong three ways (found by hand against the live units afterwards): (1) list-units without --plain prefixes a failed unit with the bullet glyph, so awk '{print $1}' yielded the glyph and the crash-looping consumer was dropped from the judged set; (2) the not-runnable skip read Requires= from systemctl show, which returns resolved + escaped names, so the AST-daemon template instance was false-skipped on an escaped slice; (3) the PROD_ROOT prefix match had no trailing slash, so /opt/orchestrata-tools/... units (ADR-0067 tool tree, the identity verifier's own root) were pulled into the product set. All three fixed in 325c721c6 (--plain; dependency names read from the unit FILE via FragmentPath; *"$PROD_ROOT/"*), 77 ship-module tests green, hand dry-run names the consumer "FAIL not active" and nothing else. Log: p6a_ship3.log.
P6a 5b fix (325c721c6 + evidence) 2026-10-07 c8c4ddd23 (2 commits fast-forwarded from ab189ccc7) ship modules 77 passed / 0 failed (hand run, ship_tests_5b.log); ship --affected 83 passed / 0 failed; gates clean Claude config (214 plugin files at c8c4ddd2), tools, gitlab-runner verified FAILED at prod graphs, rolled back to ab189ccc7. resolve, infra, tree ab189ccc7 -> c8c4ddd23, deps skip, assets, VERSION c8c4ddd23e9e, 4 services restarted, verify OK (19.9 s, identity OK, 11 units) all green; bootstrap_graphs.py --all ran the KG chain (schema, AST full sync, architecture, spec enrichment, ~7 min) and then the Memgraph step ingest_orchestrata_ultimate.py, which re-embeds all 5,740 chunks through OpenRouter (mistralai/codestral-embed-2505); the 58th embedding call returned HTTP 402 Payment Required and the stage failed at 449.8 s. Account state measured afterwards from the prod key: total_credits 1148, total_usage 1148.22 (balance exhausted; today's three promotes plus the P5 ones re-embedded the same 5,740 chunks each time). Rollback returned the tree to ab189ccc7 and restarted the 4 units; ship exited 1 before 5a/5b, so the corrected 5b has not yet run against live prod inside a ship. Prod is at ab189ccc7, master at c8c4ddd23 (2 commits ahead: the 5b fix + evidence; both are ship-side files, nothing in /opt/orchestrata needs them). Blocker: OpenRouter credit — Mohamed's. Finding F-P6a-2 (design, ADR-0074 Proposed): a prod promote's success is gated on re-embedding the whole semantic graph through a metered external API on every ship, and a payment failure rolls back an otherwise verified deploy; the ingester has no content-hash skip, and bootstrap_graphs.py already offers --kg without --memgraph and the ingester a free --backend local. Log: p6a_ship4.log.
ADR-0074 + P6a rows (6b07334ca) 2026-10-07 f21ab3af8 (2 commits) ship --affected green; gates clean Claude config, tools, gitlab-runner verified at f21ab3af8 --no-promote (a promote would re-run the same paid re-embed into the same 402 and restart-and-roll-back the prod units again; ADR-0074 records why). Prod stays at ab189ccc7. Wiki decisions.md c5a3326 shipped.
P6b promoter verify settle hold (brief 4d726c78c, 92e756596; build 7ac5e7f79; fix round 1 341678e58; record + contract D29 daec60c25) 2026-10-07 57cf64e44 (6 commits fast-forwarded from f21ab3af8) ship --affected: 171 passed / 0 failed / 0 skipped, 10 modules run + 2 reused, 71 s; builder: 101 passed across the four promotion modules, 27 in test_verify_settle_hold.py twice; coverage stages_verify.py 94% / stages_units.py 97%; ruff, black, pyright, RUL-007, check_spec_authority clean Claude config, tools, gitlab-runner verified at 57cf64e44 --no-promote (same blocker: OpenRouter balance, ADR-0074). The new hold is therefore proven on real transient units and by the reviewer's live probes, not yet by a prod promote; the first promote after the balance is restored exercises it (expected: 11-s hold on prod, max(10, 10, 5, 5) + 1). RTM: P6b cites no requirement id (nothing under promotion/ does), so no RTM row changes. Wiki modules/promotion.md 776aff8 (verify stage section). Log: p6b_ship1.log.

Prod-promotion state after P5 (2026-10-06). The two P5 blockers are gone: the plain units are retired and the test-env record is GREEN. Promotion now reaches the real prod deploy and stops at verify on three things P5 never owned: R16b (Mohamed's ruling, ADR-0072 Proposed), the verifier's treatment of tool-tree template instances (folded into P6 as its first item), and two failed timer-driven oneshots on the host (new packet PH1, "prod unit health": retention unit env + backup exit-1 diagnosis). Prod tree is at 287826a9 with the consumer failed (as before the ship). Nothing on the host was left in a worse state than before: units restarted by the promote are back on 287826a9.

Corrections from the second ship (2026-10-06). (1) Prod promotion IS coupled to this plan: the plain units fail the prod identity verify on their path directives whether or not they are running, so nothing promotes to prod until P5 removes them — that is before the conversation-consumer question (ADR-0066 R16b) is even reached. (2) Why the ad-hoc daemon keeps "going inactive": it exits cleanly after its 2 h idle timeout (code=exited, status=0 at 23:52:28 after a 2 h 00 m 12 s run) and the plain unit's Restart=on-failure never restarts a clean exit. Not the promoter's doing. P3's Restart=always is the fix for the template; the ad-hoc unit was restarted by hand at 01:5x on the standing request and is removed by P5. (3) The GREEN test-env record gate let this ship through where it had refused P1 minutes earlier; the record's scope rule is undocumented in this plan and is to be read from promote_env.py before P6 relies on it.

4. Order and ship cadence

P1 → P2 → P3 → P4 are one ship each (each independently green and useful; P3 without P2 would install a unit pointing at a binary that does not exist, so P3 ships only after P2 is deployed). P5 ships with P4 or right after. P6 follows P5. P7 rides with whichever packet last touches the file. Within a packet the AST half is built and green before the codemod half starts.

Every ship is bash deploy/ship.sh from the packet's worktree; ship's install-tools.sh arm is what deploys this, so no prod promotion is involved — this plan does not depend on the open conversation-consumer endpoint (ADR-0066 R16b).

5. Risks