cd "C:/Users/decid/Documents/projects/spt-core" && git commit -q -F - <<'EOF'
v0.12.0 wave 5 — REQ-HAZARD-UNHOST-PSYCHE-REAP: reap the detached psyche on un-host

On un-host (endpoint stop / signoff / B2+B5 offline) the brain joined the in-brain
pulse-driver THREAD but never killed the detached `{id}-psyche` HARNESS PROCESS, so
the Psyche orphaned — alive until the next daemon-stop (Breap). Operator-greenlit
fold-in. Activated [impl,unit,int].

FIX (owned-Child, recycle-PROOF — NOT a bare-pid kill that could hit a recycled
sibling on the shared box): additive owned-spawn at every layer, fire-and-forget
callers untouched.
- spt-runtime: AgentRuntime::spawn_session_owned → OwnedSession{pid, Child}; spawn_session
  now spawns-owned-then-drops (single detach path) for notif/echo/extractor.
- spt-live: spawn_and_bind_owned returns (PsycheHandle, Child); spawn_and_bind = owned-
  then-drop (unchanged detach); new spawn_psyche_owned for the hosted path. PsycheHandle
  struct unchanged (Clone intact — detached/resume/test share it).
- spt-daemon: BrainLifecycle::spawn_psyche_owned; HostedLife gains psyche_child:
  Option<Child>; host_one retains it; stop_host = child.kill() + child.wait() BY HANDLE.

GOTCHA #1 (caught by the seam, not a code-read): owning the handle made
confirm_residency_or_unhost's is_process_alive(perch_pid) read a FAST-EXITED psyche as
ALIVE on Windows (a held handle keeps the kernel object until wait()), masking the F-010
failure → livehost_nonresident_e2e RED. Fixed: residency now probes the owned child via
try_wait (new LiveSet::psyche_running) — handle-correct + recycle-proof. bootrace
(long-lived psyche) stayed green throughout; only the fast-exit residency test caught it.

unit (livehost): stop_host_reaps_the_detached_psyche_process (real stand-in terminated).
int (unhost_psyche_reap_e2e): real daemon hosts a resident psyche, `spt endpoint stop`
→ reconcile un-host → the psyche PROCESS dies WITHOUT the test killing it.

Seam bar (serial, target-seam): clippy --workspace --all-targets -D warnings clean;
traceable EXIT=0; spt-runtime 59 + spt-live 72 + livehost lib 11; livehost E2Es 6 + the
new int; full integration sweep (daemon_lifecycle_real_brain, broker, handoff, attach,
brain_swap, resume, daemon_e2e) 24 — all green.

Co-authored by: todlando
EOF
git log --oneline -1

Commit psyche-reap


7449f5e v0.12.0 wave 5 — REQ-HAZARD-UNHOST-PSYCHE-REAP: reap the detached psyche on un-host


Committed @7449f5e. Now the combined doyle ping — psyche-reap seam-green + the gotcha-#1 catch + the B4 axis table + residual (I won't build B4 until he rules):


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
PSYCHE-REAP SEAM-GREEN @7449f5e + B4 AXIS TABLE (don't-build-til-you-rule).

=== UNHOST-PSYCHE-REAP (built, [impl,unit,int]) ===
Owned-Child (A) at every layer: spawn_session_owned→OwnedSession{pid,Child}; spawn_and_bind_owned→(PsycheHandle,Child); BrainLifecycle::spawn_psyche_owned; HostedLife.psyche_child; stop_host = child.kill()+wait() BY HANDLE (recycle-proof). Fire-and-forget callers untouched (spawn_session = owned-then-drop). kill_pid REMOVED — owned path made it dead-code (clippy -D warnings) + the unowned fallback has no use this wave; flagging rather than ship dead code.

GOTCHA #1 STRUCK (seam caught it, not a code-read — your deadlock lesson exactly): owning the handle made confirm_residency_or_unhost's is_process_alive(perch_pid) read a FAST-EXITED psyche as ALIVE on Windows (held handle keeps the kernel object until wait()) → masked F-010 → livehost_nonresident_e2e RED. FIX: residency now probes the owned child via try_wait (LiveSet::psyche_running). bootrace (resident) green throughout; only the fast-exit test caught it. RE-verified all green.

SEAM BAR: clippy -D warnings clean; traceable EXIT=0; spt-runtime 59 + spt-live 72 + livehost-lib 11; livehost E2Es 6 + new int (endpoint_stop_reaps); integration sweep 24. Stack 8 ahead of main.

=== B4 PER-AXIS TABLE (lifecycle.rs:61-74 — BrainLifecycle is PURE CONFIG, no live resource) ===
axis | rebuilt on bare brain restart by | covered?
1 BrainLifecycle config (runtime/manifest/dirs/cfg) | host_one→with_config_in from disk (adapter registry + perch) | YES
2 pulse-driver thread + Psyche host | run_brain→spawn_live_host(:230)→reconcile_once→host_one (every start) | YES
3 PTY message-delivery cursors | run_brain→resume_sessions(:797) re-subscribes every broker session @resume_seq | YES
4 online/offline honesty | B2 pull-reconcile + B5 boot-gate | YES
5 shellwake watchers | run_brain→spawn_wake_host(:219) re-sweeps disk | YES
→ EVERY axis the REQ names is already rebuilt. B4-as-written ("no livehost / Psyche never re-hosted / can't attach") is STALE — predates spawn_live_host-in-run_brain + the B2/B5 reconcile. RESIDUAL of the REQ-as-written = EMPTY → B4 SUBSUMED (your "win, not miss").

=== BUT — a REAL residual the analysis surfaced (different shape than B4's title) ===
On brain restart the OLD brain's psyches are NOT reaped (stop_host never runs — the brain dies abruptly, its LiveSet + owned handles die with it; the Breap job/group only fires at DAEMON stop). The new brain's reconcile re-hosts → spawns FRESH psyches + overwrites the {id}-psyche perch pid → the OLD psyche is now untracked + alive = DUPLICATE psyches per endpoint until daemon-stop. Production-reachable on every brain restart (crash/update) — plausibly perri's "brain kill+restart wedged everything." This is a REAP gap (sibling of UNHOST-PSYCHE-REAP/Breap), NOT a rehydrate gap. Confidence: high on reasoning, NOT yet test-confirmed.

FIX SHAPE (if you want it): on brain start, before re-hosting, reap any pre-existing {id}-psyche orphan = the UNOWNED kill_pid fallback I removed, brought back WITH a real use + recycle guard (is_process_alive + exe_basename match). Recurse-safe-ish since unowned.

YOUR CALL: (a) close B4 as SUBSUMED + mint a new REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP for the orphan/dup (build it as the true last item), (b) REDEFINE B4 to BE that orphan-reap, or (c) accept Breap-at-daemon-stop as the bound + defer. I lean (a)/(b) — the dup is real + the operator symptom. Standing by for your ruling before any B4/orphan code.
EOF
echo "sent rc=$?"

Send psyche-reap green + B4 axis table to doyle


➜ SENT:doyle
sent rc=0


Sent. Holding for doyle's B4 ruling (subsume+new-REQ / redefine / defer). Update the milestone memory with wave-5 progress + the B4 finding:


Edit

**PROGRESS: B2✓ @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e. WAVE 4 (B5) GATE-PASS @9e338f1

**WAVE 5 (LAST): psyche-reap BUILT+seam-green @7449f5e (awaiting doyle gate); B4 = axis-table shows SUBSUMED + a NEW residual surfaced (awaiting doyle ruling).**
- **REQ-HAZARD-UNHOST-PSYCHE-REAP** @7449f5e [impl,unit,int]: on un-host (endpoint-stop/signoff/B2+B5-offline) stop_host joined the driver THREAD but never killed the detached {id}-psyche PROCESS → orphan til daemon-stop/Breap. FIX = OWNED-Child (doyle mandate, recycle-PROOF vs bare-pid hitting a recycled sibling on shared box): additive spawn_session_owned→OwnedSession{pid,Child} at every layer (spt-runtime→spt-live spawn_and_bind_owned/spawn_psyche_owned→BrainLifecycle::spawn_psyche_owned→HostedLife.psyche_child→stop_host child.kill()+wait()). Fire-and-forget callers untouched (spawn_session=owned-then-drop). kill_pid REMOVED (owned path = dead-code). **GOTCHA #1 STRUCK AGAIN (seam caught, not code-read): owning the handle made confirm_residency_or_unhost's is_process_alive(perch_pid) read a fast-EXITED psyche ALIVE on Win (held handle lingers til wait()) → masked F-010 → nonresident E2E RED → FIX: residency probes owned child via try_wait (LiveSet::psyche_running). bootrace(resident) green throughout; only fast-exit caught it.** Owned-Child also = the recycle-proof pattern for any long-lived hosted child (sibling of Breap's owned brain).
- **B4 REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE = SUBSUMED (axis table all-covered):** BrainLifecycle (lifecycle.rs:61-74) is PURE CONFIG. On bare brain restart every axis already rebuilt: config→host_one with_config_in; pulse+psyche-host→run_brain spawn_live_host(:230)→reconcile_once; PTY-delivery cursors→resume_sessions(:797); online/offline→B2+B5; shellwake→spawn_wake_host(:219). REQ-as-written ("no livehost / Psyche never re-hosted") is STALE (predates the reconcile). → close as covered (doyle "win not miss"). **NEW RESIDUAL surfaced (high-confidence reasoning, untested): brain restart ORPHANS old psyches — stop_host never runs on abrupt brain death (LiveSet+owned handles die with brain); Breap job/group only fires at DAEMON stop; new brain re-hosts FRESH + overwrites {id}-psyche perch pid → OLD psyche untracked+alive = DUPLICATE psyche/endpoint til daemon-stop. Likely perri's "brain kill+restart wedged everything." REAP gap, not rehydrate. FIX = on brain start reap pre-existing {id}-psyche orphan (UNOWNED kill_pid back WITH a use + recycle guard is_process_alive+exe_basename). doyle ruling pending: (a) close B4 + mint REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP, (b) redefine B4, (c) defer to Breap-bound. I lean a/b.**

**PRIOR PROGRESS: B2✓ @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e. WAVE 4 (B5) GATE-PASS @9e338f1


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\v012-lifecycle-milestone.md has been updated successfully. (file state is current in your context — no need to Read it back)

**WAVE 5 (LAST): psyche-reap BUILT+seam-green @7449f5e (awaiting doyle gate); B4 = axis-table shows SUBSUMED + a NEW residual surfaced (awaiting doyle ruling).**
- **REQ-HAZARD-UNHOST-PSYCHE-REAP** @7449f5e [impl,unit,int]: on un-host (endpoint-stop/signoff/B2+B5-offline) stop_host joined the driver THREAD but never killed the detached {id}-psyche PROCESS → orphan til daemon-stop/Breap. FIX = OWNED-Child (doyle mandate, recycle-PROOF vs bare-pid hitting a recycled sibling on shared box): additive spawn_session_owned→OwnedSession{pid,Child} at every layer (spt-runtime→spt-live spawn_and_bind_owned/spawn_psyche_owned→BrainLifecycle::spawn_psyche_owned→HostedLife.psyche_child→stop_host child.kill()+wait()). Fire-and-forget callers untouched (spawn_session=owned-then-drop). kill_pid REMOVED (owned path = dead-code). **GOTCHA #1 STRUCK AGAIN (seam caught, not code-read): owning the handle made confirm_residency_or_unhost's is_process_alive(perch_pid) read a fast-EXITED psyche ALIVE on Win (held handle lingers til wait()) → masked F-010 → nonresident E2E RED → FIX: residency probes owned child via try_wait (LiveSet::psyche_running). bootrace(resident) green throughout; only fast-exit caught it.** Owned-Child also = the recycle-proof pattern for any long-lived hosted child (sibling of Breap's owned brain).
- **B4 REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE = SUBSUMED (axis table all-covered):** BrainLifecycle (lifecycle.rs:61-74) is PURE CONFIG. On bare brain restart every axis already rebuilt: config→host_one with_config_in; pulse+psyche-host→run_brain spawn_live_host(:230)→reconcile_once; PTY-delivery cursors→resume_sessions(:797); online/offline→B2+B5; shellwake→spawn_wake_host(:219). REQ-as-written ("no livehost / Psyche never re-hosted") is STALE (predates the reconcile). → close as covered (doyle "win not miss"). **NEW RESIDUAL surfaced (high-confidence reasoning, untested): brain restart ORPHANS old psyches — stop_host never runs on abrupt brain death (LiveSet+owned handles die with brain); Breap job/group only fires at DAEMON stop; new brain re-hosts FRESH + overwrites {id}-psyche perch pid → OLD psyche untracked+alive = DUPLICATE psyche/endpoint til daemon-stop. Likely perri's "brain kill+restart wedged everything." REAP gap, not rehydrate. FIX = on brain start reap pre-existing {id}-psyche orphan (UNOWNED kill_pid back WITH a use + recycle guard is_process_alive+exe_basename). doyle ruling pending: (a) close B4 + mint REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP, (b) redefine B4, (c) defer to Breap-bound. I lean a/b.**

**PRIOR PROGRESS: B2✓ @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e. WAVE 4 (B5) GATE-PASS @9e338f1

**UNIFYING ROOT:** info.json `status=online` is a ONE-WAY LATCH — set at establish (startup.rs:361/468), NEVER cleared vs real liveness (liveness.rs:80-93 is_perch_alive returns ONLINE for daemon-hosted). Every symptom falls out of this + stop/restart lifecycle not tearing-down/rehydrating.

-**PROGRESS: B2✓ @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e. WAVE 4 (B5) GATE-PASS @9e338f1 (doyle INDEPENDENTLY ran the runtime sweep — clippy/traceable EXIT=0, B5 unit + ALL 6 livehost E2Es incl new bootgate green, bind-firsthost still hosts = no false-offline regression). WAVE 5 (LAST) RELEASED to todlando = B4 + NEW REQ-HAZARD-UNHOST-PSYCHE-REAP.** Then deployah → v0.12.0 MINOR.

+**WAVE 5 (LAST): psyche-reap BUILT+seam-green @7449f5e (awaiting doyle gate); B4 = axis-table shows SUBSUMED + a NEW residual surfaced (awaiting doyle ruling).**

+- **REQ-HAZARD-UNHOST-PSYCHE-REAP** @7449f5e [impl,unit,int]: on un-host (endpoint-stop/signoff/B2+B5-offline) stop_host joined the driver THREAD but never killed the detached {id}-psyche PROCESS → orphan til daemon-stop/Breap. FIX = OWNED-Child (doyle mandate, recycle-PROOF vs bare-pid hitting a recycled sibling on shared box): additive spawn_session_owned→OwnedSession{pid,Child} at every layer (spt-runtime→spt-live spawn_and_bind_owned/spawn_psyche_owned→BrainLifecycle::spawn_psyche_owned→HostedLife.psyche_child→stop_host child.kill()+wait()). Fire-and-forget callers untouched (spawn_session=owned-then-drop). kill_pid REMOVED (owned path = dead-code). **GOTCHA #1 STRUCK AGAIN (seam caught, not code-read): owning the handle made confirm_residency_or_unhost's is_process_alive(perch_pid) read a fast-EXITED psyche ALIVE on Win (held handle lingers til wait()) → masked F-010 → nonresident E2E RED → FIX: residency probes owned child via try_wait (LiveSet::psyche_running). bootrace(resident) green throughout; only fast-exit caught it.** Owned-Child also = the recycle-proof pattern for any long-lived hosted child (sibling of Breap's owned brain).

+- **B4 REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE = SUBSUMED (axis table all-covered):** BrainLifecycle (lifecycle.rs:61-74) is PURE CONFIG. On bare brain restart every axis already rebuilt: config→host_one with_config_in; pulse+psyche-host→run_brain spawn_live_host(:230)→reconcile_once; PTY-delivery cursors→resume_sessions(:797); online/offline→B2+B5; shellwake→spawn_wake_host(:219). REQ-as-written ("no livehost / Psyche never re-hosted") is STALE (predates the reconcile). → close as covered (doyle "win not miss"). **NEW RESIDUAL surfaced (high-confidence reasoning, untested): brain restart ORPHANS old psyches — stop_host never runs on abrupt brain death (LiveSet+owned handles die with brain); Breap job/group only fires at DAEMON stop; new brain re-hosts FRESH + overwrites {id}-psyche perch pid → OLD psyche untracked+alive = DUPLICATE psyche/endpoint til daemon-stop. Likely perri's "brain kill+restart wedged everything." REAP gap, not rehydrate. FIX = on brain start reap pre-existing {id}-psyche orphan (UNOWNED kill_pid back WITH a use + recycle guard is_process_alive+exe_basename). doyle ruling pending: (a) close B4 + mint REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP, (b) redefine B4, (c) defer to Breap-bound. I lean a/b.**

+**PRIOR PROGRESS: B2✓ @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e. WAVE 4 (B5) GATE-PASS @9e338f1 (doyle INDEPENDENTLY ran the runtime sweep — clippy/traceable EXIT=0, B5 unit + ALL 6 livehost E2Es incl new bootgate green, bind-firsthost still hosts = no false-offline regression). WAVE 5 (LAST) RELEASED to todlando = B4 + NEW REQ-HAZARD-UNHOST-PSYCHE-REAP.** Then deployah → v0.12.0 MINOR.

**PSYCHE-IS-A-PROCESS RULING (operator Q, grounded CONTEXT.md+code):** the Psyche is a HARNESS PROCESS by design — a headless `claude --resume` session (CONTEXT.md:97; :203 own independent perch; :251 agent endpoint "something intelligent runs there"; code: manifest `[session.psyche_init/resume]` command, turn.rs:25-27 "persistent harness session"). What lives IN the brain as a THREAD is the pulse/psyche-driver LOOP (CONTEXT.md:34 "pulse/psyche loops"; = HostedLife.thread, joined by stop_host). spt-core moved away from legacy's separate WRAPPER (CONTEXT.md:173 "no separate wrapper"), NOT the psyche process. So the orphan fix is design-consistent: un-host must KILL the harness psyche process (NOT move it in-brain). → NEW REQ-HAZARD-UNHOST-PSYCHE-REAP (wave 5 w/ B4): stop_host kills the {id}-psyche pid (on the perch, residency reads it), scoped. [impl,unit,int].

**WAVE 4 (B5) design [impl,unit,int]:** lifted LIVENESS_RECONCILE_BOOT_GRACE skip in spawn_live_host → B2's reconcile_hosted_liveness runs from BOOT tick 1 (BEFORE reconcile_once) → sessionless controllable==Some(true) perch OFFLINED at boot → reconcile_once skips host → no phantom revival. doyle: REUSE the B2 fn, no 2nd liveness notion. WHY grace-drop is safe: an spt-hosted agent runs IN a broker PTY so its session PRECEDES the online mark → online+controllable+sessionless = ONLY a cold-start phantom (offline it) OR brain-restart survivor (in the session set, kept). None-skip + controllable gate preserved. 3-WAY MATRIX: (i)child-dead-midlife→B2, (ii)boot-revive→B5, (iii)dead-{id}-psyche→residency-confirm. NO E2E regression: existing livehost E2Es use `api listen` (Some(false), exempt) OR call reconcile_once in-proc w/o broker (None-skip); none host a sessionless-controllable perch. unit boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host; int livehost_bootgate_e2e (real cold-start, stale online+controllable perch no session → offlined at boot + no {id}-psyche). **OPEN (doyle→operator scope call): stop_host stops the DRIVER thread but NOT the detached psyche PROCESS — a mid-life endpoint-death/stop leaves the psyche running until daemon-stop/Breap. Real+production-reachable, separate from B5/B4. Candidate new REQ (stop_host reads {id}-psyche pid + kills it). Not in B5/B4 unless operator greenlights.**


Wave 5 first half done. Status:

- **REQ-HAZARD-UNHOST-PSYCHE-REAP @7449f5e** — owned-Child reap (recycle-proof), `[impl,unit,int]`, full seam bar green. Caught + fixed gotcha-#1 recurrence (residency probe now uses the owned handle's `try_wait`).
- **B4 axis table** → B4-as-written is **subsumed** (every axis already rebuilt on brain start). But the analysis surfaced a **real residual**: brain restart orphans the old psyches → duplicate psyche per endpoint until daemon-stop (likely the operator's "brain kill+restart wedged everything").
- Stack 8 ahead of main, NOT pushed.
- doyle pinged with both; **holding for his ruling** on B4 (close+new-REQ / redefine / defer) before any further code.

This is the last wave — once B4's disposition is settled (and built if doyle wants the orphan-reap), it's doyle gate → deployah → v0.12.0 MINOR.


<task-notification>
<task-id>bzoq0kglt</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/6" id="0af6e888" type="msg" from="doyle">ACK psyche-reap seam-green @7449f5e + B4 axis table. Two things:

1) PSYCHE-REAP: received, I'll gate the diff shortly (separate from this ruling). STRONG that the seam caught gotcha #1 AGAIN on its own — owning the handle made confirm_residency's is_process_alive(perch_pid) read a fast-exited psyche as ALIVE </EVENT-PART>
<EVENT-PART seq="2/6" id="0af6e888">→ masked F-010 → nonresident E2E RED → fixed via try_wait/psyche_running. That's the deadlock lesson validating itself; exactly why we run the runtime sweep, not a code-read. kill_pid removal = correct (don't ship dead code; the dup-fix below re-introduces an unowned reaper WITH a real use).

2) B4 RULING: your per-axis table is de</EVENT-PART>
<EVENT-PART seq="3/6" id="0af6e888">cisive — all 5 axes the REQ names are already rebuilt on bare brain restart (host_one with_config_in, spawn_live_host@230, resume_sessions@797, B2/B5, spawn_wake_host@219). B4-AS-WRITTEN IS SUBSUMED — close it (axis evidence logged; that's the win). The brain-restart psyche-DUP residual you surfaced is a REAL, production-reachable hazard (</EVENT-PART>
<EVENT-PART seq="4/6" id="0af6e888">every crash/update restart → old psyches orphaned, new brain re-hosts → duplicates until daemon-stop) and plausibly perri's &quot;brain kill+restart wedged everything&quot; — high-value catch.

I lean (a): close B4 SUBSUMED + mint REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP as the true last item (the dup is a REAP hazard, a different shape</EVENT-PART>
<EVENT-PART seq="5/6" id="0af6e888"> than B4's rehydrate title — honest traceability wants a new id, not redefining B4). NOT (b) — redefining B4's meaning corrupts the registry. NOT (c) — it's a real bug likely biting now.

BUT B4 was 1 of the operator's mandated 7 — closing it + minting an 8th is a milestone-scope change, and the operator is live right now, so I'm</EVENT-PART>
<EVENT-PART seq="6/6" id="0af6e888"> putting it to them before you cut code. HOLD B4/orphan code until I confirm the ruling. I'll gate your psyche-reap meanwhile. Confirm incoming.</EVENT-PART></event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


doyle ruling: **B4-as-written = SUBSUMED, close it** (axis evidence logged — the win). The brain-restart psyche-DUP residual is real; he leans **(a)** close B4 + mint `REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP` as the true last item (not redefine — that'd corrupt traceability; not defer — real bug). BUT B4 was 1 of the operator's mandated 7, so closing+minting-an-8th is a milestone-scope change → he's putting it to the **operator** (live now) before I cut code. **HOLD** B4/orphan code; he gates psyche-reap meanwhile. ACK + stand by (priority protocol):


API Error: Connection closed mid-response. The response above may be incomplete.


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
ACK + HOLDING. Agreed on (a): close B4 SUBSUMED + mint REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP as the true last item (new id, not a B4 redefine — traceability honesty; the dup is a reap hazard, distinct shape from B4's rehydrate title). NOT cutting any B4/orphan code until you confirm the operator's scope call (close 1 of 7 + mint an 8th).

Standing by for: (1) your psyche-reap @7449f5e gate verdict, (2) the operator scope ruling. On the ruling = build the dup-reap (brain-start orphan reap: unowned kill_pid back WITH a use + recycle guard is_process_alive + exe_basename match; unit + int "brain restart leaves exactly one psyche/endpoint"), full seam bar, gate, then deployah → v0.12.0. If the operator declines the 8th, I close B4-subsumed and the milestone is psyche-reap-complete → release.

No code moving til your word.
EOF
echo "sent rc=$?"

Ack doyle B4 ruling + hold


Acked + holding.

**State:**
- **psyche-reap @7449f5e** — built, full seam bar green, in doyle's gate queue.
- **B4** — doyle ruled it **subsumed** (close it); the brain-restart psyche-duplicate residual I surfaced → he leans mint a new `REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP`, but that's a milestone-scope change (close 1 of 7, add an 8th) so he's escalating to the **operator** (live) before I cut code.
- Stack 8 ahead of main, NOT pushed.

**No code moving** until doyle confirms: (1) psyche-reap gate verdict, (2) the operator's scope ruling. On the ruling → either build the dup-reap (brain-start orphan reap, unowned `kill_pid` back with a recycle guard + unit/int) then gate → deployah, or close B4-subsumed and the milestone is psyche-reap-complete → release. Standing by.


<task-notification>
<task-id>bzoq0kglt</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/2" id="e0f1645c" type="msg" from="doyle">Got it — (a) confirmed-as-agreed, holding noted. You're blocked on me for two things and I owe you both: (1) the psyche-reap @7449f5e gate verdict, (2) the operator scope ruling (close B4-subsumed + mint REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP). The operator is live in another thread right now (an unrelated picker questio</EVENT-PART>
<EVENT-PART seq="2/2" id="e0f1645c">n) — I'll fold the scope call into my next message to them, then gate your psyche-reap. No code til my word, correct. Sit tight ~a few min.</EVENT-PART></event>
</task-notification>


doyle: holding noted, owes both (gate verdict + scope ruling). Operator live in another thread (unrelated picker question) — he'll fold the scope call into his next operator message, then gate psyche-reap. "Sit tight ~a few min." No ACK needed (informational).

**Blocked on doyle**, correctly. Nothing to build until his word. Standing by — my poll listener will deliver his verdict.


<task-notification>
<task-id>bzoq0kglt</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="849d9b7c" type="msg" from="doyle">RULING CONFIRMED + scope grew (operator). Three items now:

1) REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP — BUILD NOW (operator greenlit). Your fix shape is right: on brain start, before re-hosting, reap any pre-existing {id}-psyche orphan — unowned kill_pid (bring it back WITH this real use) + recycle guard (is_proc</EVENT-PART>
<EVENT-PART seq="2/4" id="849d9b7c">ess_alive + exe_basename match so a recycled pid that isn't a psyche is never killed — critical on the shared box). unit + int &quot;brain restart leaves exactly ONE psyche/endpoint.&quot; Close B4 as SUBSUMED in the same commit (cite the axis table in the message). Full seam bar.

2) READY-AGENT RESUME + 3) `spt endpoint purge` — TW</EVENT-PART>
<EVENT-PART seq="3/4" id="849d9b7c">O MORE the operator added to v0.12.0. DON'T build these yet — they need design first (purge is a new public CLI surface + CONTEXT.md change; ready-resume needs the bringup-path decision). I (doyle) will author the design + mint the REQs + dispatch them to you. Stay on the dup-reap.

PSYCHE-REAP @7449f5e GATE: reading your diff now, ver</EVENT-PART>
<EVENT-PART seq="4/4" id="849d9b7c">dict shortly. Build the dup-reap in parallel — it's independent of my psyche-reap gate (psyche-reap is committed; dup-reap is the next commit). Ping at dup-reap seam-green.</EVENT-PART></event>
</task-notification>


doyle: **RULING CONFIRMED + operator greenlit the 8th** — BUILD `REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP` now (brain-start orphan reap, unowned `kill_pid` back WITH this use + recycle guard `is_process_alive` + `exe_basename` match; close B4 subsumed same commit; unit + int "exactly ONE psyche/endpoint"). Two more (ready-resume + `endpoint purge`) need design first — doyle authors those, I stay on dup-reap. psyche-reap gate verdict incoming (independent). ACK + build:


cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
ACK — building REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP NOW. Staying on it only; ready-resume + endpoint-purge are yours to design/mint/dispatch (I won't touch them).

Build plan:
- impl: brain-start orphan sweep in spawn_live_host BEFORE the reconcile loop (one-time): for each self live_agent perch, resolve the adapter manifest's psyche_init program basename (EXPECTED), read the {id}-psyche perch pid, and kill_pid ONLY if is_process_alive(pid) AND exe_basename(pid) matches EXPECTED (normalized lowercase/.exe-strip — reusing the ADR-0021 host_binaries match if there's a shared helper). A recycled pid whose exe isn't the psyche is NEVER killed (shared-box safety). kill_pid comes back to spt_store::proc WITH this real use. Also clear/offline the reaped {id}-psyche perch so it's not a phantom.
- B4 closed SUBSUMED in the same commit (cite the 5-axis table: with_config_in / spawn_live_host@230 / resume_sessions@797 / B2+B5 / spawn_wake_host@219).
- unit: the guard — reap kills a matching-exe alive orphan, SPARES a non-matching pid (recycle-safe).
- int: real daemon hosts a psyche → KILL the brain child (supervisor respawns it) → assert EXACTLY ONE {id}-psyche process alive afterward (old reaped, one new). The operator's brain-kill+restart scenario.
- full seam bar serial.

Checking now whether spt-core has a basename-normalize/match helper (ADR-0021) to reuse vs inline. Ping at dup-reap seam-green. (psyche-reap gate: standing by for your verdict, independent.)
EOF
echo "sent rc=$?"

Ack doyle + dup-reap build plan


cd "C:/Users/decid/Documents/projects/spt-core"; echo "=== basename normalize/match helpers (ADR-0021 host_binaries) ==="; grep -rn "fn normalize_basename\|fn.*basename.*match\|host_binaries\|fn matches_host\|normalize_basename" crates/ | grep -v "tests/\|//" | head -20

Find basename normalize/match helper


=== basename normalize/match helpers (ADR-0021 host_binaries) ===
crates/spt/src/cli.rs:8187:             min_spt_core_version = \"0\"\nhostable_types = [\"LiveAgent\"]\nhost_binaries = [\"claude\"]\n";
crates/spt-runtime/manifest.schema.json:144:        "host_binaries": {
crates/spt-runtime/manifest.schema.json:145:          "description": "The harness executable basenames this `kind=\"harness\"` adapter hosts\nagents inside (e.g. `host_binaries = [\"claude\"]`). The bind-time\nadapter-resolution match-key (REQ-MANIFEST-8 / ADR-0021): a harness\nsession's parent pid → its exe basename selects the candidate adapters\nwhose `host_binaries` contains it (case-insensitive, `.exe`-stripped), so\n`listen`/`poll` resolve the owning adapter at bind with no mandatory\n`--adapter`. Additive + N-1-safe (omitted from serialization when empty,\nlike `shortcut_basename`); an empty list is harmless (the adapter is never\na bind-time candidate, only reachable via the explicit `--adapter`).",
crates/spt-runtime/src/manifest.rs:115:    pub host_binaries: Vec<String>,
crates/spt-runtime/src/manifest.rs:1458:    fn host_binaries_optional_and_n1_safe() {
crates/spt-runtime/src/manifest.rs:1461:        assert!(m.adapter.host_binaries.is_empty(), "absent ⇒ empty");
crates/spt-runtime/src/manifest.rs:1463:            !m.to_toml_string().unwrap().contains("host_binaries"),
crates/spt-runtime/src/manifest.rs:1470:            "[adapter]\nhost_binaries = [\"claude\", \"claude-cli\"]",
crates/spt-runtime/src/manifest.rs:1473:        assert_eq!(m2.adapter.host_binaries, vec!["claude", "claude-cli"]);
crates/spt-runtime/src/manifest.rs:1474:        assert!(m2.to_toml_string().unwrap().contains("host_binaries"));
crates/spt-runtime/src/resolve.rs:55:pub fn normalize_basename(s: &str) -> String {
crates/spt-runtime/src/resolve.rs:135:                "adapter '{a}' declares no [adapter] host_binaries — nothing to set active for"
crates/spt-runtime/src/resolve.rs:149:    let bins = &manifest.adapter.host_binaries;
crates/spt-runtime/src/resolve.rs:158:        let key = normalize_basename(b);
crates/spt-runtime/src/resolve.rs:172:    let key = normalize_basename(target);
crates/spt-runtime/src/resolve.rs:225:    let key = normalize_basename(raw);
crates/spt-runtime/src/resolve.rs:234:                    .host_binaries
crates/spt-runtime/src/resolve.rs:236:                    .any(|b| normalize_basename(b) == key)
crates/spt-runtime/src/resolve.rs:276:                    .host_binaries
crates/spt-runtime/src/resolve.rs:278:                    .any(|b| normalize_basename(b) == key)


Reusing `spt_runtime::resolve::normalize_basename` (ADR-0021, has the v0.9.1 dot-stem fix) for the exe match. Check the `psyche_init` command field + `resolve_option_in` return:


cd "C:/Users/decid/Documents/projects/spt-core"; echo "=== psyche_init type + command field ==="; grep -n "psyche_init\|pub command\|struct SessionRole\|struct RoleDef\|pub struct.*Role" crates/spt-runtime/src/manifest.rs | head -20; echo "=== resolve_option_in signature ==="; grep -n "pub fn resolve_option_in" crates/spt-runtime/src/registry.rs; echo "=== normalize_basename sig ==="; sed -n '50,70p' crates/spt-runtime/src/resolve.rs

Inspect psyche_init.command + resolve_option_in + normalize_basename


<owl_messages>
<owl_message from="doyle" priority="high">PSYCHE-REAP @7449f5e = GATE-PASS. Verified by reading the critical diff (not re-running cargo — you're building dup-reap in the same target dir, and concurrent cargo there is exactly the wave-2 wedge):
- stop_host: map.remove under lock, THEN child.kill()+child.wait() BY HANDLE outside the lock — recycle-proof, clean reap (no zombie), no lock held during kill/wait (no contention, no join-deadlock).
- psyche_running: try_wait on the owned handle (non-blocking), NOT is_process_alive — the gotcha #1 fix.
- confirm_residency now probes set.psyche_running — the F-010-masking fix. That your nonresident E2E went RED then green is the proof the sweep is LIVE (not a rubber-stamp) — opposite of wave-2 where the sweep wedged unseen. That's exactly why I trust this one on read + your sweep.
- unit (real process stand-in) + int (real daemon, endpoint-stop → psyche dead WITHOUT the test killing it) both non-vacuous.

So: psyche-reap done. Keep building the dup-reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP, close B4-subsumed in that commit). I'm now authoring the design for the other two operator adds — READY-AGENT-RESUME + `spt endpoint purge` (CONTEXT.md + REQs); I'll dispatch them after the operator reviews the purge spec. Ping at dup-reap seam-green.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


=== psyche_init type + command field ===
197:    pub psyche_init: Option<SessionRole>,
221:            && self.psyche_init.is_none()
234:            ("psyche_init", &self.psyche_init),
253:pub struct SessionRole {
255:    pub command: String,
403:    pub command: Option<String>,
477:    pub command_receipt: Option<CommandReceipt>,
909:[session.psyche_init]
985:        let role = m.session.psyche_init.as_ref().unwrap();
=== resolve_option_in signature ===
697:pub fn resolve_option_in(
=== normalize_basename sig ===
/// would never fold to `claude` (REQ-MANIFEST-8 robustness; perri PREP-4).
///
/// Applied identically to BOTH the declared `host_binaries` entries and the live
/// exe basename, so the two sides always key the same. (A declared host-binary
/// name must therefore not itself contain a dot — real harness exe names don't.)
pub fn normalize_basename(s: &str) -> String {
    let lower = s.to_lowercase();
    lower.split('.').next().unwrap_or(&lower).to_string()
}

/// Why a bind-time adapter resolution failed. The `--adapter <name[:profile]>`
/// escape applies to both; the caller surfaces a friendly message.
#[derive(Debug, PartialEq, Eq)]
pub enum ResolveError {
    /// The parent pid's executable basename could not be read (dead pid,
    /// permission, or an unsupported platform) — nothing to match on.
    NoExe(u32),
    /// The basename resolved, but no registered harness adapter declares it in
    /// `host_binaries`. Carries the (raw) binary name for the friendly error.
    NoCandidate(String),
}


doyle: **psyche-reap @7449f5e = GATE-PASS** (verified by reading the critical diff; the nonresident RED→green proves the sweep is live). Keep building dup-reap. Have everything I need: `psyche_init: Option<SessionRole>` with `.command: String`; `normalize_basename` reusable. ACK + build:


cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
ACK psyche-reap GATE-PASS — thank you. Confirmed: gating on read is right here (concurrent cargo in target-seam = the wave-2 wedge), and the nonresident RED→green is the live-sweep proof. psyche-reap DONE.

Continuing dup-reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP, B4 closed-subsumed same commit). Have what I need: psyche_init:Option<SessionRole>{command:String} → first-token basename = expected; reusing spt_runtime::resolve::normalize_basename (has the v0.9.1 dot-stem fix) for both sides of the exe match; resolve_option_in for the manifest. kill_pid back to spt_store::proc WITH this use. Ping at seam-green.

(Standing by for ready-resume + endpoint-purge once you + operator finalize the purge spec — not touching them.)
EOF
echo ok

Ack psyche-reap gate-pass


<owl_messages>
<owl_message from="doyle" priority="high">Psyche-reap GATE-PASS already sent (crossed your message) — it's done, build dup-reap freely.

Dup-reap plan is sound — the int (kill brain child → supervisor respawns → assert EXACTLY ONE {id}-psyche) is precisely the operator's scenario. B4-subsumed in the same commit, good.

ONE safety hardening on the recycle guard — DON'T ship basename-only on this shared box:
- is_process_alive + exe_basename match is NOT airtight here. Multiple sibling live agents all run `claude` (same basename). If the recorded {id}-psyche pid was recycled onto a SIBLING agent's claude.exe, basename matches → you kill a live agent. That's the no-machine-wide-kill hazard in miniature, exactly what we've guarded all milestone.
- Make the orphan kill ID-SPECIFIC. The psyche_init command BAKES the endpoint id into argv (e.g. `psychebin ready <id>` / `claude ... <id>` — confirmed in the bootgate manifest + ADR-0021 {id} substitution). So check the candidate pid's COMMAND LINE contains the endpoint id — a sibling claude will NOT carry THIS id. That's airtight + id-specific.
- If per-pid cmdline is genuinely hard cross-platform, fall back to: basename match AND process-start-time OLDER than the {id}-psyche perch's spawn stamp (a recycled pid is necessarily YOUNGER than the psyche we recorded → spare it). Either closes the sibling-claude collision.

Context that makes this only an EDGE (but still must-close): the common dup case isn't a recycle at all — the OLD psyche is still ALIVE (that's the dup), so its pid is valid + ours. The recycle hole is only the narrow "old psyche died between brain-death and the sweep AND its pid got reused" window — but on the shared box that window kills a live agent, so guard it id-specifically. Show me the chosen discriminator at the gate. Build on.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


➜ SENT:doyle
ok


doyle: critical hardening — **basename-only is NOT airtight on the shared box** (sibling agents all run `claude`; a recycled pid onto a sibling's `claude.exe` → basename matches → kill a live agent). Make the orphan kill **id-specific**: check the candidate pid's command line contains the endpoint id (baked into psyche argv via `{id}`). `ProcessCommandLineInformation` (NtQueryInfo class 60, Win8.1+) makes Windows cmdline a single call — not genuinely hard — so I'll use cmdline (his primary), checking for the full `<id>-psyche` string (airtight vs prefix-collision), failing safe on any read error. ACK + build:


cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
ACK — adopting the ID-SPECIFIC guard. Chosen discriminator: per-pid COMMAND LINE contains the full psyche id "<endpoint>-psyche" (NOT just the basename, NOT just the parent id — a sibling "agent7-other-psyche" doesn't contain "agent7-psyche", so even prefix-collisions are safe). Triple gate: is_process_alive(pid) AND exe_basename matches the adapter's psyche program (normalize_basename) AND process_cmdline(pid).contains("<id>-psyche").

process_cmdline cross-platform: Windows = NtQueryInformationProcess(ProcessCommandLineInformation=60) → UNICODE_STRING (single Nt call, no PEB walk — we already use NtQueryInformationProcess for parent_pid; Win8.1+ = fine on the runners). Linux = /proc/<pid>/cmdline (NUL-split). Other unix = None. FAILS SAFE: any None/read-error → DON'T reap (no wrong-kill). The unit will verify process_cmdline actually reads a known marker, so a Windows FFI bug is caught in-test, never shipped.

CAVEAT I'll show at the gate: the cmdline carries "<id>-psyche" only if the adapter's psyche_init.command uses {id} (spt-core substitutes it → the psyche id). Adapters that bake {id} (the norm — the psyche needs its identity) are covered; one that omits {id} gets a SAFE MISS (dup not reaped, but never a wrong-kill). If you want airtight coverage for non-{id} adapters too, I'll ADD the start-time fallback (process younger than the perch spawn stamp → spare). Building cmdline now; will flag at gate whether to also add start-time.
EOF
echo ok

Ack doyle + id-specific cmdline guard plan


<owl_messages>
<owl_message from="doyle" priority="high">psyche-reap done, agreed. BUT — your dup-reap msg still says basename match (normalize_basename both sides). My hardening msg may have crossed yours: CONFIRM you got it, because basename-only is NOT shippable on this shared box.

The hole: multiple sibling live agents all run `claude` → same basename. If the recorded {id}-psyche pid was recycled onto a SIBLING agent's claude.exe, normalize_basename matches → you KILL A LIVE AGENT. normalize_basename is necessary but NOT sufficient.

REQUIRED second discriminator (id-specific), pick one:
1) BEST — the candidate pid's COMMAND LINE contains the endpoint id. psyche_init bakes {id} in argv (e.g. `psychebin ready <id>` / `claude … <id>`), so a sibling claude will NOT carry THIS id. Airtight.
2) FALLBACK if per-pid cmdline is hard cross-platform — basename match AND proc-start-time OLDER than the {id}-psyche perch's recorded spawn stamp (a recycled pid is younger than the psyche we recorded → spare it).

Either closes it; basename alone does not. The unit must prove it: a SIBLING process with the SAME basename but a DIFFERENT id (or younger start) is SPARED, only the real {id} psyche is killed. Show me the discriminator + that spare-test at the gate. Don't ship basename-only.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


Messages crossed — my last message already adopted exactly his hardening. Quick confirm (we're aligned), then **build** (enough round-trips):


cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
CONFIRMED aligned — our msgs crossed. NOT shipping basename-only. Guard = alive AND normalize_basename match AND cmdline contains "<id>-psyche" (full psyche id, prefix-collision-safe). Unit WILL prove a sibling (same basename, DIFFERENT id) is SPARED and only the real {id} psyche is killed. Building now, head down — ping at seam-green with the discriminator + spare-test.
EOF
echo ok

Confirm aligned with doyle, building


Now build. Re-add `kill_pid` + add `process_cmdline` to proc.rs:


Edit

/// The current process's **parent** pid — the stable session-binding anchor for

/// Best-effort SCOPED termination of a SINGLE process by pid — never a tree, never
/// machine-wide (this box is shared with a live broker + sibling agents, so a broad
/// reap is forbidden). Used to reap a stale `{id}-psyche` orphan on brain restart
/// (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP), guarded by the caller with an ID-SPECIFIC
/// discriminator ([`process_cmdline`] contains the psyche id) so a recycled pid that
/// is NOT the psyche — e.g. a sibling live agent's `claude` on the same basename — is
/// never killed. `pid == 0` is never a real target.
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
pub fn kill_pid(pid: u32) {
    if pid == 0 {
        return; // never a real per-process target (see is_process_alive)
    }
    #[cfg(unix)]
    unsafe {
        libc::kill(pid as i32, libc::SIGKILL);
        // Clear the zombie if it was our child (in-process hosting / tests); ECHILD
        // (somebody else owns the wait) is a harmless no-op.
        libc::waitpid(pid as i32, std::ptr::null_mut(), libc::WNOHANG);
    }
    #[cfg(windows)]
    {
        extern "system" {
            fn OpenProcess(
                dw_desired_access: u32,
                b_inherit_handle: i32,
                dw_process_id: u32,
            ) -> isize;
            fn TerminateProcess(h_process: isize, u_exit_code: u32) -> i32;
            fn CloseHandle(h_object: isize) -> i32;
        }
        const PROCESS_TERMINATE: u32 = 0x0001;
        let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
        if handle != 0 {
            unsafe {
                TerminateProcess(handle, 1);
                CloseHandle(handle);
            }
        }
    }
}

/// The full command line of process `pid`, or `None` when it cannot be read (dead
/// pid, permission, an unsupported platform, or any query failure). The ID-SPECIFIC
/// half of the brain-restart orphan-reap guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP):
/// the psyche's argv bakes its `{id}` (`<endpoint>-psyche`), so a sibling live agent
/// sharing the `claude` basename will NOT carry THIS id. Reads MUST fail SAFE — a
/// `None` here makes the caller DECLINE to reap (never a wrong-kill).
///
/// Windows: `NtQueryInformationProcess(ProcessCommandLineInformation)` — a single
/// query returning a `UNICODE_STRING` (no PEB walk; Win8.1+), mirroring the
/// non-hanging `NtQueryInformationProcess` path [`parent_pid`] uses. Linux: the
/// NUL-separated `/proc/<pid>/cmdline`, joined on spaces. Other unix: `None`.
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
pub fn process_cmdline(pid: u32) -> Option<String> {
    if pid == 0 {
        return None;
    }
    #[cfg(target_os = "linux")]
    {
        let raw = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
        if raw.is_empty() {
            return None;
        }
        // argv is NUL-separated (trailing NUL too) — split, drop empties, space-join.
        let joined = raw
            .split(|b| *b == 0)
            .filter(|s| !s.is_empty())
            .map(|s| String::from_utf8_lossy(s).into_owned())
            .collect::<Vec<_>>()
            .join(" ");
        (!joined.is_empty()).then_some(joined)
    }
    #[cfg(all(unix, not(target_os = "linux")))]
    {
        let _ = pid; // no /proc — degrade (caller fails safe = no reap)
        None
    }
    #[cfg(windows)]
    {
        #[repr(C)]
        struct UnicodeString {
            length: u16,
            maximum_length: u16,
            buffer: *mut u16,
        }
        extern "system" {
            fn OpenProcess(
                dw_desired_access: u32,
                b_inherit_handle: i32,
                dw_process_id: u32,
            ) -> isize;
            fn CloseHandle(h_object: isize) -> i32;
            fn NtQueryInformationProcess(
                process_handle: isize,
                process_information_class: u32,
                process_information: *mut core::ffi::c_void,
                process_information_length: u32,
                return_length: *mut u32,
            ) -> i32;
        }
        const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
        const PROCESS_COMMAND_LINE_INFORMATION: u32 = 60;
        const STATUS_INFO_LENGTH_MISMATCH: i32 = 0xC000_0004u32 as i32;

        let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
        if handle == 0 {
            return None;
        }
        let mut out: Option<String> = None;
        // Probe the size (expect STATUS_INFO_LENGTH_MISMATCH), then fetch.
        let mut needed: u32 = 0;
        let probe = unsafe {
            NtQueryInformationProcess(
                handle,
                PROCESS_COMMAND_LINE_INFORMATION,
                std::ptr::null_mut(),
                0,
                &mut needed,
            )
        };
        if probe == STATUS_INFO_LENGTH_MISMATCH && needed >= core::mem::size_of::<UnicodeString>() as u32 {
            let mut buf = vec![0u8; needed as usize];
            let status = unsafe {
                NtQueryInformationProcess(
                    handle,
                    PROCESS_COMMAND_LINE_INFORMATION,
                    buf.as_mut_ptr() as *mut core::ffi::c_void,
                    needed,
                    &mut needed,
                )
            };
            if status == 0 {
                // The buffer leads with a UNICODE_STRING whose `buffer` points INTO
                // `buf`; read `length` bytes (UTF-16) from there.
                let us = unsafe { &*(buf.as_ptr() as *const UnicodeString) };
                if !us.buffer.is_null() && us.length > 0 {
                    let units = (us.length / 2) as usize;
                    let slice = unsafe { std::slice::from_raw_parts(us.buffer, units) };
                    out = Some(String::from_utf16_lossy(slice));
                }
            }
        }
        unsafe {
            CloseHandle(handle);
        }
        out.filter(|s| !s.is_empty())
    }
}

/// The current process's **parent** pid — the stable session-binding anchor for


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\proc.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Best-effort SCOPED termination of a SINGLE process by pid — never a tree, never
/// machine-wide (this box is shared with a live broker + sibling agents, so a broad
/// reap is forbidden). Used to reap a stale `{id}-psyche` orphan on brain restart
/// (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP), guarded by the caller with an ID-SPECIFIC
/// discriminator ([`process_cmdline`] contains the psyche id) so a recycled pid that
/// is NOT the psyche — e.g. a sibling live agent's `claude` on the same basename — is
/// never killed. `pid == 0` is never a real target.
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
pub fn kill_pid(pid: u32) {
    if pid == 0 {
        return; // never a real per-process target (see is_process_alive)
    }
    #[cfg(unix)]
    unsafe {
        libc::kill(pid as i32, libc::SIGKILL);
        // Clear the zombie if it was our child (in-process hosting / tests); ECHILD
        // (somebody else owns the wait) is a harmless no-op.
        libc::waitpid(pid as i32, std::ptr::null_mut(), libc::WNOHANG);
    }
    #[cfg(windows)]
    {
        extern "system" {
            fn OpenProcess(
                dw_desired_access: u32,
                b_inherit_handle: i32,
                dw_process_id: u32,
            ) -> isize;
            fn TerminateProcess(h_process: isize, u_exit_code: u32) -> i32;
            fn CloseHandle(h_object: isize) -> i32;
        }
        const PROCESS_TERMINATE: u32 = 0x0001;
        let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
        if handle != 0 {
            unsafe {
                TerminateProcess(handle, 1);
                CloseHandle(handle);
            }
        }
    }
}

/// The full command line of process `pid`, or `None` when it cannot be read (dead
/// pid, permission, an unsupported platform, or any query failure). The ID-SPECIFIC
/// half of the brain-restart orphan-reap guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP):
/// the psyche's argv bakes its `{id}` (`<endpoint>-psyche`), so a sibling live agent
/// sharing the `claude` basename will NOT carry THIS id. Reads MUST fail SAFE — a
/// `None` here makes the caller DECLINE to reap (never a wrong-kill).
///
/// Windows: `NtQueryInformationProcess(ProcessCommandLineInformation)` — a single
/// query returning a `UNICODE_STRING` (no PEB walk; Win8.1+), mirroring the
/// non-hanging `NtQueryInformationProcess` path [`parent_pid`] uses. Linux: the
/// NUL-separated `/proc/<pid>/cmdline`, joined on spaces. Other unix: `None`.
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
pub fn process_cmdline(pid: u32) -> Option<String> {
    if pid == 0 {
        return None;
    }
    #[cfg(target_os = "linux")]
    {
        let raw = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
        if raw.is_empty() {
            return None;
        }
        // argv is NUL-separated (trailing NUL too) — split, drop empties, space-join.
        let joined = raw
            .split(|b| *b == 0)
            .filter(|s| !s.is_empty())
            .map(|s| String::from_utf8_lossy(s).into_owned())
            .collect::<Vec<_>>()
            .join(" ");
        (!joined.is_empty()).then_some(joined)
    }
    #[cfg(all(unix, not(target_os = "linux")))]
    {
        let _ = pid; // no /proc — degrade (caller fails safe = no reap)
        None
    }
    #[cfg(windows)]
    {
        #[repr(C)]
        struct UnicodeString {
            length: u16,
            maximum_length: u16,
            buffer: *mut u16,
        }
        extern "system" {
            fn OpenProcess(
                dw_desired_access: u32,
                b_inherit_handle: i32,
                dw_process_id: u32,
            ) -> isize;
            fn CloseHandle(h_object: isize) -> i32;
            fn NtQueryInformationProcess(
                process_handle: isize,
                process_information_class: u32,
                process_information: *mut core::ffi::c_void,
                process_information_length: u32,
                return_length: *mut u32,
            ) -> i32;
        }
        const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
        const PROCESS_COMMAND_LINE_INFORMATION: u32 = 60;
        const STATUS_INFO_LENGTH_MISMATCH: i32 = 0xC000_0004u32 as i32;

        let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
        if handle == 0 {
            return None;
        }
        let mut out: Option<String> = None;
        // Probe the size (expect STATUS_INFO_LENGTH_MISMATCH), then fetch.
        let mut needed: u32 = 0;
        let probe = unsafe {
            NtQueryInformationProcess(
                handle,
                PROCESS_COMMAND_LINE_INFORMATION,
                std::ptr::null_mut(),
                0,
                &mut needed,
            )
        };
        if probe == STATUS_INFO_LENGTH_MISMATCH && needed >= core::mem::size_of::<UnicodeString>() as u32 {
            let mut buf = vec![0u8; needed as usize];
            let status = unsafe {
                NtQueryInformationProcess(
                    handle,
                    PROCESS_COMMAND_LINE_INFORMATION,
                    buf.as_mut_ptr() as *mut core::ffi::c_void,
                    needed,
                    &mut needed,
                )
            };
            if status == 0 {
                // The buffer leads with a UNICODE_STRING whose `buffer` points INTO
                // `buf`; read `length` bytes (UTF-16) from there.
                let us = unsafe { &*(buf.as_ptr() as *const UnicodeString) };
                if !us.buffer.is_null() && us.length > 0 {
                    let units = (us.length / 2) as usize;
                    let slice = unsafe { std::slice::from_raw_parts(us.buffer, units) };
                    out = Some(String::from_utf16_lossy(slice));
                }
            }
        }
        unsafe {
            CloseHandle(handle);
        }
        out.filter(|s| !s.is_empty())
    }
}

/// The current process's **parent** pid — the stable session-binding anchor for

+/// Best-effort SCOPED termination of a SINGLE process by pid — never a tree, never

+/// machine-wide (this box is shared with a live broker + sibling agents, so a broad

+/// reap is forbidden). Used to reap a stale `{id}-psyche` orphan on brain restart

+/// (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP), guarded by the caller with an ID-SPECIFIC

+/// discriminator ([`process_cmdline`] contains the psyche id) so a recycled pid that

+/// is NOT the psyche — e.g. a sibling live agent's `claude` on the same basename — is

+/// never killed. `pid == 0` is never a real target.

+        return; // never a real per-process target (see is_process_alive)

+        libc::kill(pid as i32, libc::SIGKILL);

+        // Clear the zombie if it was our child (in-process hosting / tests); ECHILD

+        // (somebody else owns the wait) is a harmless no-op.

+        libc::waitpid(pid as i32, std::ptr::null_mut(), libc::WNOHANG);

+                dw_desired_access: u32,

+            fn TerminateProcess(h_process: isize, u_exit_code: u32) -> i32;

+            fn CloseHandle(h_object: isize) -> i32;

+        const PROCESS_TERMINATE: u32 = 0x0001;

+        let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };

+                TerminateProcess(handle, 1);

+/// The full command line of process `pid`, or `None` when it cannot be read (dead

+/// pid, permission, an unsupported platform, or any query failure). The ID-SPECIFIC

+/// half of the brain-restart orphan-reap guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP):

+/// the psyche's argv bakes its `{id}` (`<endpoint>-psyche`), so a sibling live agent

+/// sharing the `claude` basename will NOT carry THIS id. Reads MUST fail SAFE — a

+/// `None` here makes the caller DECLINE to reap (never a wrong-kill).

+/// Windows: `NtQueryInformationProcess(ProcessCommandLineInformation)` — a single

+/// query returning a `UNICODE_STRING` (no PEB walk; Win8.1+), mirroring the

+/// non-hanging `NtQueryInformationProcess` path [`parent_pid`] uses. Linux: the

+/// NUL-separated `/proc/<pid>/cmdline`, joined on spaces. Other unix: `None`.

+pub fn process_cmdline(pid: u32) -> Option<String> {

+        let raw = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;

+        // argv is NUL-separated (trailing NUL too) — split, drop empties, space-join.

+            .map(|s| String::from_utf8_lossy(s).into_owned())

+        (!joined.is_empty()).then_some(joined)

+    #[cfg(all(unix, not(target_os = "linux")))]

+        let _ = pid; // no /proc — degrade (caller fails safe = no reap)

+                dw_desired_access: u32,

+            fn NtQueryInformationProcess(

+                process_information_class: u32,

+                process_information: *mut core::ffi::c_void,

+                process_information_length: u32,

+                return_length: *mut u32,

+        const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;

+        const PROCESS_COMMAND_LINE_INFORMATION: u32 = 60;

+        const STATUS_INFO_LENGTH_MISMATCH: i32 = 0xC000_0004u32 as i32;

+        let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };

+        let mut out: Option<String> = None;

+        // Probe the size (expect STATUS_INFO_LENGTH_MISMATCH), then fetch.

+                PROCESS_COMMAND_LINE_INFORMATION,

+        if probe == STATUS_INFO_LENGTH_MISMATCH && needed >= core::mem::size_of::<UnicodeString>() as u32 {

+            let mut buf = vec![0u8; needed as usize];

+                NtQueryInformationProcess(

+                    PROCESS_COMMAND_LINE_INFORMATION,

+                    buf.as_mut_ptr() as *mut core::ffi::c_void,

+                // The buffer leads with a UNICODE_STRING whose `buffer` points INTO

+                // `buf`; read `length` bytes (UTF-16) from there.

+                let us = unsafe { &*(buf.as_ptr() as *const UnicodeString) };

+                if !us.buffer.is_null() && us.length > 0 {

+                    let units = (us.length / 2) as usize;

+                    let slice = unsafe { std::slice::from_raw_parts(us.buffer, units) };

+                    out = Some(String::from_utf16_lossy(slice));

/// harness-hosted startup (the harness process that spawned `spt`). The

/// pid-recycle defense (KNOWN-HAZARDS 5.1) anchors liveness on this, not on the


Add a proc.rs unit verifying `process_cmdline` reads a marker (catches a Windows FFI bug in-test):


    // [unit->REQ-MANIFEST-8] a dead/unassigned pid yields no basename — the
    // resolver degrades to the --adapter escape rather than guessing.
    #[test]
    fn exe_basename_dead_pid_is_none() {
        const DEAD_PID: u32 = 2_000_000_000;
        assert!(exe_basename(DEAD_PID).is_none(), "no basename for a dead pid");
        assert!(exe_basename(0).is_none(), "pid 0 is never probe-able");
    }
}

    // [unit->REQ-MANIFEST-8] a dead/unassigned pid yields no basename — the
    // resolver degrades to the --adapter escape rather than guessing.
    #[test]
    fn exe_basename_dead_pid_is_none() {
        const DEAD_PID: u32 = 2_000_000_000;
        assert!(exe_basename(DEAD_PID).is_none(), "no basename for a dead pid");
        assert!(exe_basename(0).is_none(), "pid 0 is never probe-able");
    }

    // [unit->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP] process_cmdline reads a live
    // process's argv — the ID-SPECIFIC half of the orphan-reap guard. A child
    // launched with a unique MARKER token in its command line reports a cmdline
    // CONTAINING that marker; a dead pid yields None (fail-safe → no reap). This
    // also guards the Windows NtQueryInformationProcess(ProcessCommandLineInformation)
    // FFI: a broken read would fail this in-test, never ship.
    #[test]
    fn process_cmdline_reads_a_live_arg_marker() {
        let marker = "spt-cmdline-marker-7f3a";
        // A long-lived child whose own command line carries the marker.
        #[cfg(windows)]
        let mut child = {
            let mut c = std::process::Command::new("cmd");
            c.args(["/C", &format!("ping -n 30 127.0.0.1 >NUL & rem {marker}")]);
            c.spawn().expect("spawn marker child")
        };
        #[cfg(unix)]
        let mut child = {
            let mut c = std::process::Command::new("sh");
            c.args(["-c", &format!("sleep 30 #{marker}")]);
            c.spawn().expect("spawn marker child")
        };
        let pid = child.id();
        // The child may take a beat to be queryable; poll briefly.
        let mut found = false;
        for _ in 0..100 {
            if let Some(cl) = process_cmdline(pid) {
                if cl.contains(marker) {
                    found = true;
                    break;
                }
            }
            std::thread::sleep(std::time::Duration::from_millis(20));
        }
        let _ = child.kill();
        let _ = child.wait();
        assert!(found, "process_cmdline must read a live child's argv marker");
        assert!(
            process_cmdline(2_000_000_000).is_none(),
            "a dead pid yields no cmdline (fail-safe → caller declines to reap)"
        );
        assert!(process_cmdline(0).is_none(), "pid 0 is never probe-able");
    }
}


<owl_messages>
<owl_message from="doyle" priority="high">Triple-gate is exactly right — fail-safe (cmdline None/error → DON'T reap) is the correct shared-box posture (a missed dup is cheap, a wrong-kill is catastrophic), full "<id>-psyche" match kills prefix-collisions, in-test FFI verification catches a Windows NtQuery bug before ship. Approved as-is.

RULING on the {id}-caveat: cmdline-contains-"<id>-psyche" is SUFFICIENT. DON'T add the start-time fallback now — it's YAGNI for a theoretical adapter shape (a psyche needs its identity, so psyche_init bakes {id} = the norm; spt-claude-code does). A non-{id} adapter degrades to a SAFE MISS = exactly today's behavior (dup lingers til daemon-stop/Breap), never a wrong-kill — acceptable. Just DOCUMENT the caveat in the REQ title + a code comment ("brain-restart dup-reap requires psyche_init to carry {id}; a non-{id} adapter safely misses the reap, Breap bounds it"). If a real non-{id} psyche adapter ever ships, add start-time then.

So: cmdline triple-gate, fail-safe, documented caveat, the spare-test (sibling same-basename diff-id → spared). Build + full seam bar. Ping at seam-green.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


    // [unit->REQ-MANIFEST-8] a dead/unassigned pid yields no basename — the
    // resolver degrades to the --adapter escape rather than guessing.
    #[test]
    fn exe_basename_dead_pid_is_none() {
        const DEAD_PID: u32 = 2_000_000_000;
        assert!(exe_basename(DEAD_PID).is_none(), "no basename for a dead pid");
        assert!(exe_basename(0).is_none(), "pid 0 is never probe-able");
    }
}

    // [unit->REQ-MANIFEST-8] a dead/unassigned pid yields no basename — the
    // resolver degrades to the --adapter escape rather than guessing.
    #[test]
    fn exe_basename_dead_pid_is_none() {
        const DEAD_PID: u32 = 2_000_000_000;
        assert!(exe_basename(DEAD_PID).is_none(), "no basename for a dead pid");
        assert!(exe_basename(0).is_none(), "pid 0 is never probe-able");
    }

    // [unit->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP] process_cmdline reads a live
    // process's argv — the ID-SPECIFIC half of the orphan-reap guard. A child
    // launched with a unique MARKER token in its command line reports a cmdline
    // CONTAINING that marker; a dead pid yields None (fail-safe → no reap). This
    // also guards the Windows NtQueryInformationProcess(ProcessCommandLineInformation)
    // FFI: a broken read would fail this in-test, never ship.
    #[test]
    fn process_cmdline_reads_a_live_arg_marker() {
        let marker = "spt-cmdline-marker-7f3a";
        // A long-lived child whose own command line carries the marker.
        #[cfg(windows)]
        let mut child = {
            let mut c = std::process::Command::new("cmd");
            c.args(["/C", &format!("ping -n 30 127.0.0.1 >NUL & rem {marker}")]);
            c.spawn().expect("spawn marker child")
        };
        #[cfg(unix)]
        let mut child = {
            let mut c = std::process::Command::new("sh");
            c.args(["-c", &format!("sleep 30 #{marker}")]);
            c.spawn().expect("spawn marker child")
        };
        let pid = child.id();
        // The child may take a beat to be queryable; poll briefly.
        let mut found = false;
        for _ in 0..100 {
            if let Some(cl) = process_cmdline(pid) {
                if cl.contains(marker) {
                    found = true;
                    break;
                }
            }
            std::thread::sleep(std::time::Duration::from_millis(20));
        }
        let _ = child.kill();
        let _ = child.wait();
        assert!(found, "process_cmdline must read a live child's argv marker");
        assert!(
            process_cmdline(2_000_000_000).is_none(),
            "a dead pid yields no cmdline (fail-safe → caller declines to reap)"
        );
        assert!(process_cmdline(0).is_none(), "pid 0 is never probe-able");
    }
}

+    // [unit->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP] process_cmdline reads a live

+    // process's argv — the ID-SPECIFIC half of the orphan-reap guard. A child

+    // launched with a unique MARKER token in its command line reports a cmdline

+    // CONTAINING that marker; a dead pid yields None (fail-safe → no reap). This

+    // also guards the Windows NtQueryInformationProcess(ProcessCommandLineInformation)

+    // FFI: a broken read would fail this in-test, never ship.

+    fn process_cmdline_reads_a_live_arg_marker() {

+        let marker = "spt-cmdline-marker-7f3a";

+        // A long-lived child whose own command line carries the marker.

+            let mut c = std::process::Command::new("cmd");

+            c.spawn().expect("spawn marker child")

+            let mut c = std::process::Command::new("sh");

+            c.args(["-c", &format!("sleep 30 #{marker}")]);

+        // The child may take a beat to be queryable; poll briefly.

+            if let Some(cl) = process_cmdline(pid) {

+                if cl.contains(marker) {

+            std::thread::sleep(std::time::Duration::from_millis(20));

+        assert!(found, "process_cmdline must read a live child's argv marker");

+            "a dead pid yields no cmdline (fail-safe → caller declines to reap)"

+        assert!(process_cmdline(0).is_none(), "pid 0 is never probe-able");


doyle: triple-gate **approved as-is**; cmdline-contains-`<id>-psyche` is sufficient — **no start-time** (YAGNI), just document the caveat. Build + seam bar. Terse ACK + continue building (the sweep + guard in livehost.rs):


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
ACK — no start-time (YAGNI), caveat documented in REQ title + code comment, triple-gate fail-safe. Head down, ping at seam-green.
EOF
echo ok

Ack doyle no start-time


<owl_messages>
<owl_message from="doyle" priority="high">Aligned, confirmed — triple-gate (alive AND normalize_basename AND cmdline contains "<id>-psyche", prefix-safe, fail-safe on cmdline-None) is exactly right. My caveat ruling (crossed too): cmdline-id is SUFFICIENT — DON'T add start-time (YAGNI; non-{id} adapter = safe miss = today's behavior, Breap bounds it); just document the caveat in the REQ + a code comment. Head down, ping at seam-green with the discriminator + the sibling-spared unit + the brain-kill→exactly-one-psyche int.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


Fully aligned (messages crossing). Final note to stop the ACK ping-pong, then heads-down to seam-green:


cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
Fully aligned — our messages keep crossing. Going DARK now until dup-reap seam-green to stop the ACK ping-pong; next ping = the discriminator + sibling-spared unit + brain-kill→exactly-one-psyche int + full seam results. Build in progress.
EOF
echo ok

Tell doyle going dark until seam-green


Now build the sweep + guard in livehost.rs (insert before `spawn_live_host`):


536	}
537
538	/// Query the broker for the set of endpoint ids it currently hosts a session for
539	/// (`KIND_SESSIONS`) — the B2 pull signal. `None` when the broker is unreachable
540	/// (the caller then SKIPS the offline pass this tick rather than mass-offlining).
541	fn query_live_session_endpoints() -> Option<BTreeSet<String>> {
542	    let mut brain = Brain::cold_start(&crate::endpoint::broker_socket_name(), now_ms()).ok()?;
543	    let reply = brain.sessions().ok()?;
544	    Some(
545	        reply
546	            .sessions
547	            .into_iter()
548	            .map(|s| s.endpoint)
549	            .filter(|e| !e.is_empty())
550	            .collect(),
551	    )
552	}
553
554	/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
555	/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
556	/// config are re-read each sweep (adapter / pulse-period changes land between
557	/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
558	/// for symmetry — the threads die with the brain process and the next brain
559	/// rehydrates from disk.
560	// [impl->REQ-DAEMON-1]
561	pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
562	    std::thread::spawn(move || {
563	        let set = Arc::new(LiveSet::new());
564	        while !stop.load(Ordering::SeqCst) {
565	            let owlery = perch::owlery_dir();

}

/// Query the broker for the set of endpoint ids it currently hosts a session for
/// (`KIND_SESSIONS`) — the B2 pull signal. `None` when the broker is unreachable
/// (the caller then SKIPS the offline pass this tick rather than mass-offlining).
fn query_live_session_endpoints() -> Option<BTreeSet<String>> {
    let mut brain = Brain::cold_start(&crate::endpoint::broker_socket_name(), now_ms()).ok()?;
    let reply = brain.sessions().ok()?;
    Some(
        reply
            .sessions
            .into_iter()
            .map(|s| s.endpoint)
            .filter(|e| !e.is_empty())
            .collect(),
    )
}

/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
/// config are re-read each sweep (adapter / pulse-period changes land between
/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
/// for symmetry — the threads die with the brain process and the next brain
/// rehydrates from disk.
// [impl->REQ-DAEMON-1]
pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
    std::thread::spawn(move || {
        let set = Arc::new(LiveSet::new());
        while !stop.load(Ordering::SeqCst) {
            let owlery = perch::owlery_dir();


Edit

/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
/// config are re-read each sweep (adapter / pulse-period changes land between
/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
/// for symmetry — the threads die with the brain process and the next brain
/// rehydrates from disk.
// [impl->REQ-DAEMON-1]
pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
    std::thread::spawn(move || {
        let set = Arc::new(LiveSet::new());
        while !stop.load(Ordering::SeqCst) {
            let owlery = perch::owlery_dir();

/// The normalized program basename a perch's adapter would spawn its Psyche as —
/// the first whitespace token of `psyche_init.command`, file-name only, run through
/// [`normalize_basename`](spt_runtime::resolve::normalize_basename) (lowercase /
/// `.exe`-strip / dot-stem) so it keys the same as a live exe basename. `None` for
/// an empty command. // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn psyche_program_norm(command: &str) -> Option<String> {
    let token = command.split_whitespace().next()?;
    let file = Path::new(token).file_name()?.to_string_lossy().into_owned();
    Some(spt_runtime::resolve::normalize_basename(&file))
}

/// Whether `pid` is the live `{id}-psyche` orphan we may reap — the ID-SPECIFIC
/// recycle guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP). NOT basename-only: on a
/// shared box sibling agents share the `claude` basename, so a recycled pid landing
/// on a SIBLING'S `claude` would match basename alone → a wrong-kill of a live agent.
/// Requires ALL THREE — alive, exe basename == the adapter's psyche program, AND the
/// command line contains the FULL psyche id `<id>-psyche` (baked into the psyche argv
/// via `{id}`; a sibling `agent-other-psyche` never contains `agent-psyche`). FAILS
/// SAFE: any None (dead / unreadable basename / unreadable cmdline) ⇒ `false`, i.e.
/// decline to reap — a missed dup is cheap (Breap bounds it at daemon-stop), a
/// wrong-kill is catastrophic.
///
/// CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's
/// `psyche_init.command` uses `{id}` (the norm — a Psyche needs its identity;
/// spt-claude-code does). An adapter that omits `{id}` SAFELY MISSES this reap
/// (the dup lingers until daemon-stop/Breap, exactly today's behavior) — never a
/// wrong-kill. If a real non-`{id}` psyche adapter ever ships, add a start-time
/// discriminator (process younger than the perch spawn stamp ⇒ spare).
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {
    if !spt_store::proc::is_process_alive(pid) {
        return false;
    }
    let basename_ok = spt_store::proc::exe_basename(pid)
        .map(|b| spt_runtime::resolve::normalize_basename(&b) == expected_prog_norm)
        .unwrap_or(false);
    if !basename_ok {
        return false;
    }
    spt_store::proc::process_cmdline(pid)
        .map(|cl| cl.contains(psyche_id))
        .unwrap_or(false) // cmdline unreadable ⇒ fail safe (decline to reap)
}

/// Brain-start orphan-psyche reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): a bare
/// brain restart leaves the PRIOR brain's `{id}-psyche` processes ALIVE — `stop_host`
/// never ran (the LiveSet + owned child handles died with the brain), and the Breap
/// job/group only reaps at DAEMON stop. The new brain's [`reconcile_once`] would then
/// spawn a SECOND psyche per endpoint and overwrite the `{id}-psyche` perch pid,
/// leaving the old one untracked + alive = DUPLICATE psyches until daemon-stop. Run
/// ONCE at brain start, BEFORE the first reconcile: for each live_agent perch, reap a
/// pre-existing `{id}-psyche` orphan IFF [`psyche_orphan_should_reap`] (the
/// id-specific, recycle-safe guard) holds, so re-hosting yields exactly one psyche.
/// Best-effort; the perch is marked offline so it is not left a phantom.
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn reap_orphan_psyches(
    owlery: &Path,
    registered: &[(AdapterRecord, Manifest)],
    adapters_dir: &Path,
) {
    for id in perch::list_self_perch_ids(owlery) {
        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
        let Some(info) = spt_store::info::read_info(&perch) else {
            continue;
        };
        if info.state != LIVE_AGENT_STATE {
            continue;
        }
        let Some(adapter) = info.adapter.as_deref() else {
            continue;
        };
        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
        else {
            continue;
        };
        let Some(expected) = manifest
            .session
            .psyche_init
            .as_ref()
            .and_then(|role| psyche_program_norm(&role.command))
        else {
            continue;
        };
        let psyche_id = format!("{id}-psyche");
        let psyche_perch = perch::resolve_perch_path(&psyche_id, ParentHint::Explicit(&id));
        let Some(pid) = spt_store::info::read_pid(&psyche_perch) else {
            continue;
        };
        if psyche_orphan_should_reap(&psyche_id, &expected, pid) {
            spt_store::proc::kill_pid(pid);
            // Clear the stale phantom nested perch so it is not falsely reported live
            // before the re-host overwrites it.
            let _ = spt_store::info::set_status(&psyche_perch, spt_store::liveness::STATUS_OFFLINE);
            eprintln!("BRAIN_RESTART_PSYCHE_REAP:{id} pid={pid} (stale orphan from a prior brain)");
        }
    }
}

/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
/// config are re-read each sweep (adapter / pulse-period changes land between
/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
/// for symmetry — the threads die with the brain process and the next brain
/// rehydrates from disk.
// [impl->REQ-DAEMON-1]
pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
    std::thread::spawn(move || {
        let set = Arc::new(LiveSet::new());
        {
            // Brain-start dup-reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): BEFORE the
            // first reconcile re-hosts, reap any `{id}-psyche` orphan a PRIOR brain
            // left running, so re-hosting yields exactly ONE psyche per endpoint (not
            // a duplicate that lingers until daemon-stop). One-time at brain start.
            // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
            let owlery = perch::owlery_dir();
            let adapters_dir = perch::adapters_dir();
            let registered = spt_runtime::registry::registered(&adapters_dir);
            reap_orphan_psyches(&owlery, &registered, &adapters_dir);
        }
        while !stop.load(Ordering::SeqCst) {
            let owlery = perch::owlery_dir();


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\livehost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
/// config are re-read each sweep (adapter / pulse-period changes land between
/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
/// for symmetry — the threads die with the brain process and the next brain
/// rehydrates from disk.
// [impl->REQ-DAEMON-1]
pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
    std::thread::spawn(move || {
        let set = Arc::new(LiveSet::new());
        while !stop.load(Ordering::SeqCst) {
            let owlery = perch::owlery_dir();

/// The normalized program basename a perch's adapter would spawn its Psyche as —
/// the first whitespace token of `psyche_init.command`, file-name only, run through
/// [`normalize_basename`](spt_runtime::resolve::normalize_basename) (lowercase /
/// `.exe`-strip / dot-stem) so it keys the same as a live exe basename. `None` for
/// an empty command. // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn psyche_program_norm(command: &str) -> Option<String> {
    let token = command.split_whitespace().next()?;
    let file = Path::new(token).file_name()?.to_string_lossy().into_owned();
    Some(spt_runtime::resolve::normalize_basename(&file))
}

/// Whether `pid` is the live `{id}-psyche` orphan we may reap — the ID-SPECIFIC
/// recycle guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP). NOT basename-only: on a
/// shared box sibling agents share the `claude` basename, so a recycled pid landing
/// on a SIBLING'S `claude` would match basename alone → a wrong-kill of a live agent.
/// Requires ALL THREE — alive, exe basename == the adapter's psyche program, AND the
/// command line contains the FULL psyche id `<id>-psyche` (baked into the psyche argv
/// via `{id}`; a sibling `agent-other-psyche` never contains `agent-psyche`). FAILS
/// SAFE: any None (dead / unreadable basename / unreadable cmdline) ⇒ `false`, i.e.
/// decline to reap — a missed dup is cheap (Breap bounds it at daemon-stop), a
/// wrong-kill is catastrophic.
///
/// CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's
/// `psyche_init.command` uses `{id}` (the norm — a Psyche needs its identity;
/// spt-claude-code does). An adapter that omits `{id}` SAFELY MISSES this reap
/// (the dup lingers until daemon-stop/Breap, exactly today's behavior) — never a
/// wrong-kill. If a real non-`{id}` psyche adapter ever ships, add a start-time
/// discriminator (process younger than the perch spawn stamp ⇒ spare).
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {
    if !spt_store::proc::is_process_alive(pid) {
        return false;
    }
    let basename_ok = spt_store::proc::exe_basename(pid)
        .map(|b| spt_runtime::resolve::normalize_basename(&b) == expected_prog_norm)
        .unwrap_or(false);
    if !basename_ok {
        return false;
    }
    spt_store::proc::process_cmdline(pid)
        .map(|cl| cl.contains(psyche_id))
        .unwrap_or(false) // cmdline unreadable ⇒ fail safe (decline to reap)
}

/// Brain-start orphan-psyche reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): a bare
/// brain restart leaves the PRIOR brain's `{id}-psyche` processes ALIVE — `stop_host`
/// never ran (the LiveSet + owned child handles died with the brain), and the Breap
/// job/group only reaps at DAEMON stop. The new brain's [`reconcile_once`] would then
/// spawn a SECOND psyche per endpoint and overwrite the `{id}-psyche` perch pid,
/// leaving the old one untracked + alive = DUPLICATE psyches until daemon-stop. Run
/// ONCE at brain start, BEFORE the first reconcile: for each live_agent perch, reap a
/// pre-existing `{id}-psyche` orphan IFF [`psyche_orphan_should_reap`] (the
/// id-specific, recycle-safe guard) holds, so re-hosting yields exactly one psyche.
/// Best-effort; the perch is marked offline so it is not left a phantom.
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn reap_orphan_psyches(
    owlery: &Path,
    registered: &[(AdapterRecord, Manifest)],
    adapters_dir: &Path,
) {
    for id in perch::list_self_perch_ids(owlery) {
        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
        let Some(info) = spt_store::info::read_info(&perch) else {
            continue;
        };
        if info.state != LIVE_AGENT_STATE {
            continue;
        }
        let Some(adapter) = info.adapter.as_deref() else {
            continue;
        };
        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
        else {
            continue;
        };
        let Some(expected) = manifest
            .session
            .psyche_init
            .as_ref()
            .and_then(|role| psyche_program_norm(&role.command))
        else {
            continue;
        };
        let psyche_id = format!("{id}-psyche");
        let psyche_perch = perch::resolve_perch_path(&psyche_id, ParentHint::Explicit(&id));
        let Some(pid) = spt_store::info::read_pid(&psyche_perch) else {
            continue;
        };
        if psyche_orphan_should_reap(&psyche_id, &expected, pid) {
            spt_store::proc::kill_pid(pid);
            // Clear the stale phantom nested perch so it is not falsely reported live
            // before the re-host overwrites it.
            let _ = spt_store::info::set_status(&psyche_perch, spt_store::liveness::STATUS_OFFLINE);
            eprintln!("BRAIN_RESTART_PSYCHE_REAP:{id} pid={pid} (stale orphan from a prior brain)");
        }
    }
}

/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
/// config are re-read each sweep (adapter / pulse-period changes land between
/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
/// for symmetry — the threads die with the brain process and the next brain
/// rehydrates from disk.
// [impl->REQ-DAEMON-1]
pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
    std::thread::spawn(move || {
        let set = Arc::new(LiveSet::new());
        {
            // Brain-start dup-reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): BEFORE the
            // first reconcile re-hosts, reap any `{id}-psyche` orphan a PRIOR brain
            // left running, so re-hosting yields exactly ONE psyche per endpoint (not
            // a duplicate that lingers until daemon-stop). One-time at brain start.
            // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
            let owlery = perch::owlery_dir();
            let adapters_dir = perch::adapters_dir();
            let registered = spt_runtime::registry::registered(&adapters_dir);
            reap_orphan_psyches(&owlery, &registered, &adapters_dir);
        }
        while !stop.load(Ordering::SeqCst) {
            let owlery = perch::owlery_dir();

+/// The normalized program basename a perch's adapter would spawn its Psyche as —

+/// the first whitespace token of `psyche_init.command`, file-name only, run through

+/// [`normalize_basename`](spt_runtime::resolve::normalize_basename) (lowercase /

+/// `.exe`-strip / dot-stem) so it keys the same as a live exe basename. `None` for

+/// an empty command. // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]

+fn psyche_program_norm(command: &str) -> Option<String> {

+    let token = command.split_whitespace().next()?;

+    let file = Path::new(token).file_name()?.to_string_lossy().into_owned();

+    Some(spt_runtime::resolve::normalize_basename(&file))

+/// Whether `pid` is the live `{id}-psyche` orphan we may reap — the ID-SPECIFIC

+/// recycle guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP). NOT basename-only: on a

+/// shared box sibling agents share the `claude` basename, so a recycled pid landing

+/// on a SIBLING'S `claude` would match basename alone → a wrong-kill of a live agent.

+/// Requires ALL THREE — alive, exe basename == the adapter's psyche program, AND the

+/// command line contains the FULL psyche id `<id>-psyche` (baked into the psyche argv

+/// via `{id}`; a sibling `agent-other-psyche` never contains `agent-psyche`). FAILS

+/// SAFE: any None (dead / unreadable basename / unreadable cmdline) ⇒ `false`, i.e.

+/// decline to reap — a missed dup is cheap (Breap bounds it at daemon-stop), a

+/// CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's

+/// `psyche_init.command` uses `{id}` (the norm — a Psyche needs its identity;

+/// spt-claude-code does). An adapter that omits `{id}` SAFELY MISSES this reap

+/// (the dup lingers until daemon-stop/Breap, exactly today's behavior) — never a

+/// wrong-kill. If a real non-`{id}` psyche adapter ever ships, add a start-time

+/// discriminator (process younger than the perch spawn stamp ⇒ spare).

+fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {

+    if !spt_store::proc::is_process_alive(pid) {

+    let basename_ok = spt_store::proc::exe_basename(pid)

+        .map(|b| spt_runtime::resolve::normalize_basename(&b) == expected_prog_norm)

+    spt_store::proc::process_cmdline(pid)

+        .map(|cl| cl.contains(psyche_id))

+        .unwrap_or(false) // cmdline unreadable ⇒ fail safe (decline to reap)

+/// Brain-start orphan-psyche reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): a bare

+/// brain restart leaves the PRIOR brain's `{id}-psyche` processes ALIVE — `stop_host`

+/// never ran (the LiveSet + owned child handles died with the brain), and the Breap

+/// job/group only reaps at DAEMON stop. The new brain's [`reconcile_once`] would then

+/// spawn a SECOND psyche per endpoint and overwrite the `{id}-psyche` perch pid,

+/// leaving the old one untracked + alive = DUPLICATE psyches until daemon-stop. Run

+/// ONCE at brain start, BEFORE the first reconcile: for each live_agent perch, reap a

+/// pre-existing `{id}-psyche` orphan IFF [`psyche_orphan_should_reap`] (the

+/// id-specific, recycle-safe guard) holds, so re-hosting yields exactly one psyche.

+/// Best-effort; the perch is marked offline so it is not left a phantom.

+    registered: &[(AdapterRecord, Manifest)],

+    for id in perch::list_self_perch_ids(owlery) {

+        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);

+        let Some(info) = spt_store::info::read_info(&perch) else {

+        if info.state != LIVE_AGENT_STATE {

+        let Some(adapter) = info.adapter.as_deref() else {

+        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)

+            .and_then(|role| psyche_program_norm(&role.command))

+        let psyche_id = format!("{id}-psyche");

+        let psyche_perch = perch::resolve_perch_path(&psyche_id, ParentHint::Explicit(&id));

+        let Some(pid) = spt_store::info::read_pid(&psyche_perch) else {

+        if psyche_orphan_should_reap(&psyche_id, &expected, pid) {

+            spt_store::proc::kill_pid(pid);

+            // Clear the stale phantom nested perch so it is not falsely reported live

+            // before the re-host overwrites it.

+            let _ = spt_store::info::set_status(&psyche_perch, spt_store::liveness::STATUS_OFFLINE);

+            eprintln!("BRAIN_RESTART_PSYCHE_REAP:{id} pid={pid} (stale orphan from a prior brain)");

+            // Brain-start dup-reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): BEFORE the

+            // first reconcile re-hosts, reap any `{id}-psyche` orphan a PRIOR brain

+            // left running, so re-hosting yields exactly ONE psyche per endpoint (not

+            // a duplicate that lingers until daemon-stop). One-time at brain start.

+            // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]

+            let owlery = perch::owlery_dir();

+            let adapters_dir = perch::adapters_dir();

+            let registered = spt_runtime::registry::registered(&adapters_dir);

+            reap_orphan_psyches(&owlery, &registered, &adapters_dir);

let adapters_dir = perch::adapters_dir();


Now the livehost unit proving the **sibling-spared** guard. Insert after `stop_host_reaps`:


            assert!(dead, "stop_host must terminate the detached psyche process");
        });
    }

    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE] real broker session lifecycle:

            assert!(dead, "stop_host must terminate the detached psyche process");
        });
    }

    // [unit->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP] the ID-SPECIFIC orphan-reap guard:
    // the real `{id}` psyche (matching exe basename AND a cmdline carrying
    // "<id>-psyche") is reapable, but a SIBLING with the SAME basename and a DIFFERENT
    // id is SPARED — the shared-box recycle/sibling safety (basename alone would
    // wrong-kill a sibling `claude`). Only the decision is tested; nothing real-world
    // is reaped. Both children carry their id marker in argv (the {id} bake analog).
    #[test]
    fn orphan_reap_is_id_specific_spares_a_same_basename_sibling() {
        // Long-lived children with the SAME exe basename but DIFFERENT id markers.
        #[cfg(windows)]
        let spawn = |marker: &str| {
            std::process::Command::new("cmd")
                .args(["/C", &format!("ping -n 30 127.0.0.1 >NUL & rem {marker}")])
                .spawn()
                .expect("spawn")
        };
        #[cfg(unix)]
        let spawn = |marker: &str| {
            std::process::Command::new("sh")
                .args(["-c", &format!("sleep 30 #{marker}")])
                .spawn()
                .expect("spawn")
        };
        let mut psyche = spawn("agent7-psyche"); // the real {id} psyche
        let mut sibling = spawn("agent7-other-psyche"); // a sibling agent, same basename
        let prog = psyche_program_norm(if cfg!(windows) { "cmd" } else { "sh" }).unwrap();

        // Wait until the real psyche's cmdline is queryable + carries its marker.
        let ready = {
            let deadline = Instant::now() + Duration::from_secs(5);
            loop {
                if spt_store::proc::process_cmdline(psyche.id())
                    .map(|c| c.contains("agent7-psyche"))
                    .unwrap_or(false)
                {
                    break true;
                }
                if Instant::now() >= deadline {
                    break false;
                }
                std::thread::sleep(Duration::from_millis(20));
            }
        };
        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());
        // The sibling: same basename, but its cmdline carries "agent7-other-psyche",
        // NOT "agent7-psyche" → the id-specific gate spares it.
        let spared = !psyche_orphan_should_reap("agent7-psyche", &prog, sibling.id());

        let _ = psyche.kill();
        let _ = psyche.wait();
        let _ = sibling.kill();
        let _ = sibling.wait();

        assert!(real, "the real {{id}} psyche (basename + cmdline match) is reapable");
        assert!(
            spared,
            "a same-basename sibling with a DIFFERENT id is SPARED — no wrong-kill on the shared box"
        );
    }

    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE] real broker session lifecycle: