155	///
156	/// It inherits the broker's already-unelevated token as a plain child, so it
157	/// runs no de-elevation guard of its own.
158	// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
159	pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {
160	    let name = broker_socket_name();
161	    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;
162	    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the
163	    // generation is broker-owned custody of the KH-2.4 counter (no longer the
164	    // brain→brain BrainState.generation, which retires in D4), and the reason is
165	    // Q4's update-vs-crash discriminator that D5 will use to decide whether to
166	    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5
167	    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the
168	    // *counter* is rehydrated, never the wall-clock stamp.
169	    eprintln!(
170	        "BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})",
171	        std::process::id(),
172	        reason.as_arg(),
173	    );
174
175	    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild
176	    // continuity from the broker — re-attach EVERY hosted session in resume mode
177	    // from its delivered cursor, the production replacement for the retired
178	    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no
179	    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),
180	    // so this is a no-op now and forward-correct when daemon-hosted sessions land
181	    // (the live-agent adapter). A failure here is non-fatal — the brain still
182	    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY
183	    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the
184	    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`
185	    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).
186	    match brain.resume_sessions() {
187	        Ok(ids) if !ids.is_empty() => {
188	            eprintln!(
189	                "BRAIN_RESUMED: re-attached {} session(s): {ids:?}",
190	                ids.len()
191	            )
192	        }
193	        Ok(_) => {}
194	        Err(e) => eprintln!("BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing"),
195	    }
196
197	    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018
198	    // Q7): "ready" is the design's "re-attached all sessions + resumed loops",
199	    // not merely "socket connected". The supervisor's readiness gate (D6-2)
200	    // promotes the trial binary on this stamp; promoting on a bare post-connect
201	    // breadcrumb would accept a brain that connected but never rebuilt
202	    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so
203	    // this is forward-correct: the gate is defined on the real readiness
204	    // semantic the live-agent adapter inherits.
205	    write_ready(generation);
206
207	    // Shell wake-watchers (restoration D2-2): the offline half of the shell
208	    // online/offline exclusivity — a reconcile loop that runs each offline
209	    // instance's `wake_command` as a supervised watcher child. Relocated from
210	    // the broker process to the brain child: shellwake is brain-owned logic
211	    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the
212	    // broker over the same socket IPC for the remote-wake dial. Its boot sweep
213	    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher
214	    // children from disk — exactly the Q5 "re-reconciled on brain start"
215	    // posture. The stop flag is held for symmetry, never raised (the watchers
216	    // die with the brain process and the next brain re-sweeps them).
217	    // [impl->REQ-SHELL-2]
218	    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
219	    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));
220
221	    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller
222	    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at
223	    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting
224	    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the
225	    // shell waker). Online-only is staged behind the interim `api listen`
226	    // (status-absent perches are skipped — no double-host) until W0.2 makes the
227	    // brain the first-host. The stop flag is held for symmetry; the threads die
228	    // with the brain and the next brain rehydrates from disk (CONTEXT:194).
229	    // [impl->REQ-DAEMON-1]
230	    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);
231
232	    let mut consumers_started = false;
233	    loop {
234	        thread::sleep(BRAIN_HEARTBEAT);
235	        // `net-status` is both the net-capability probe and the broker-liveness
236	        // probe: an `Err` is the broker gone (or a future planned
237	        // snapshot+self-exit, Q3) — exit for a supervised respawn.
238	        let status = match brain.net_status() {
239	            Ok(s) => s,
240	            Err(e) => {
241	                eprintln!("BRAIN_BROKER_LOST: {e} — exiting for supervised respawn");
242	                return Err(e);
243	            }
244	        };
245	        if consumer_gate(status.enabled, consumers_started) {
246	            let hex = status.node_id_hex.clone().unwrap_or_default();
247	            spawn_net_consumers(&hex);
248	            consumers_started = true;
249	            eprintln!(
250	                "BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started"
251	            );
252	        }
253	        write_ready(generation);
254	    }
255	}
256
257	/// Whether to spawn the net consumers this heartbeat: only once the broker
258	/// reports a net endpoint up (`net_enabled`) AND we have not already started
259	/// them. Pure for the unit table — the spawn-exactly-once + net-gating

///
/// It inherits the broker's already-unelevated token as a plain child, so it
/// runs no de-elevation guard of its own.
// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {
    let name = broker_socket_name();
    let mut brain = connect_retry(&name, BRAIN_CONNECT_TRIES)?;
    // The broker hands {generation, start-reason} at spawn (ADR-0018 V2): the
    // generation is broker-owned custody of the KH-2.4 counter (no longer the
    // brain→brain BrainState.generation, which retires in D4), and the reason is
    // Q4's update-vs-crash discriminator that D5 will use to decide whether to
    // preserve or reset phase-significant loop timing. D3-2 surfaces them; D5
    // consumes the reason. `gen_start` stays fresh-now per KH-2.4 — only the
    // *counter* is rehydrated, never the wall-clock stamp.
    eprintln!(
        "BRAIN_UP: connected to broker {name} (pid {}, generation {generation}, start-reason {})",
        std::process::id(),
        reason.as_arg(),
    );

    // Multi-session cold-start resume (restoration D4-2, ADR-0018 Q6): rebuild
    // continuity from the broker — re-attach EVERY hosted session in resume mode
    // from its delivered cursor, the production replacement for the retired
    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no
    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),
    // so this is a no-op now and forward-correct when daemon-hosted sessions land
    // (the live-agent adapter). A failure here is non-fatal — the brain still
    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY
    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the
    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`
    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).
    match brain.resume_sessions() {
        Ok(ids) if !ids.is_empty() => {
            eprintln!(
                "BRAIN_RESUMED: re-attached {} session(s): {ids:?}",
                ids.len()
            )
        }
        Ok(_) => {}
        Err(e) => eprintln!("BRAIN_RESUME_NONFATAL: {e} — no sessions resumed, continuing"),
    }

    // Signal READY only now — AFTER session resume (restoration D6-1b, ADR-0018
    // Q7): "ready" is the design's "re-attached all sessions + resumed loops",
    // not merely "socket connected". The supervisor's readiness gate (D6-2)
    // promotes the trial binary on this stamp; promoting on a bare post-connect
    // breadcrumb would accept a brain that connected but never rebuilt
    // continuity. Today resume is a near-noop (no daemon-hosted sessions), so
    // this is forward-correct: the gate is defined on the real readiness
    // semantic the live-agent adapter inherits.
    write_ready(generation);

    // Shell wake-watchers (restoration D2-2): the offline half of the shell
    // online/offline exclusivity — a reconcile loop that runs each offline
    // instance's `wake_command` as a supervised watcher child. Relocated from
    // the broker process to the brain child: shellwake is brain-owned logic
    // (ADR-0018 Q5 exception), a pure disk-reconciler that only reaches the
    // broker over the same socket IPC for the remote-wake dial. Its boot sweep
    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher
    // children from disk — exactly the Q5 "re-reconciled on brain start"
    // posture. The stop flag is held for symmetry, never raised (the watchers
    // die with the brain process and the next brain re-sweeps them).
    // [impl->REQ-SHELL-2]
    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
    let _ = crate::shellwake::spawn_wake_host(Arc::new(AtomicBool::new(false)));

    // Live-agent Psyche host (M11-W0.1, REQ-DAEMON-1): the production caller
    // `run_pulse_loop` lacked. One thread sweeps `livehost::reconcile_once` at
    // boot (the rehydrate the no-op above can't do) and every tick — re-hosting
    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the
    // shell waker). Online-only is staged behind the interim `api listen`
    // (status-absent perches are skipped — no double-host) until W0.2 makes the
    // brain the first-host. The stop flag is held for symmetry; the threads die
    // with the brain and the next brain rehydrates from disk (CONTEXT:194).
    // [impl->REQ-DAEMON-1]
    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);

    let mut consumers_started = false;
    loop {
        thread::sleep(BRAIN_HEARTBEAT);
        // `net-status` is both the net-capability probe and the broker-liveness
        // probe: an `Err` is the broker gone (or a future planned
        // snapshot+self-exit, Q3) — exit for a supervised respawn.
        let status = match brain.net_status() {
            Ok(s) => s,
            Err(e) => {
                eprintln!("BRAIN_BROKER_LOST: {e} — exiting for supervised respawn");
                return Err(e);
            }
        };
        if consumer_gate(status.enabled, consumers_started) {
            let hex = status.node_id_hex.clone().unwrap_or_default();
            spawn_net_consumers(&hex);
            consumers_started = true;
            eprintln!(
                "BRAIN_NET_CONSUMERS_UP: net enabled (node {hex}) — dispatcher + peer pump started"
            );
        }
        write_ready(generation);
    }
}

/// Whether to spawn the net consumers this heartbeat: only once the broker
/// reports a net endpoint up (`net_enabled`) AND we have not already started
/// them. Pure for the unit table — the spawn-exactly-once + net-gating


Source verification (brainproc.rs:159–255) settles the H1 tension: `run_brain` connects to the **local broker** socket (:161, `?`→abort), which binds **independent of net** (broker.rs:575). Net-less, that connect succeeds → `:230 spawn_live_host` IS reached, on its own thread, before the net-gated consumer loop. So **H1 refuted by source** (live reconcile runs net-less); the heartbeat net-gates only dispatcher/peer-pump, not livehost.

The report's H2 "load-order race" mechanism is internally inconsistent (resolve + install_dir `find` use the *same* `registered` slice — can't succeed-then-miss), and a transient miss self-heals in 5s ≠ persistent. So the true failure is **runtime-observable only**: most likely `spawn_psyche` persistently fails (`LIVEHOST_PSYCHE_FAIL`, REQ-INSTALL-11 binary-not-found), or the broker doesn't spawn the brain child net-less (H3, unverified). doyle steered correctly: **the real-daemon E2E is both the gate artifact and the reproduction** — build it, watch it fail, fix from observed reality.

Recording verified findings + wave plan to memory (context insurance) before building:


Write

---
name: v081-livehost-bootrace
description: "v0.8.1 task — fix REQ-HAZARD-LIVEHOST-BOOT-RACE: net-less node, live agent goes online but daemon never hosts Psyche (no LIVEHOST_PSYCHE). doyle delegation, I own waves+execution. Verified root-cause-so-far + wave plan + the non-negotiable real-daemon E2E gate."
metadata:
  node_type: memory
  type: project
---

**v0.8.1 livehost boot-race** — doyle delegation (operator-directed), I own W/T planning + execution, doyle gates + drives release (counter 17). Branch `v0.8.1-livehost-bootrace` off main (v0.8.0), base @2aab895 mints `REQ-HAZARD-LIVEHOST-BOOT-RACE` (required_stages=[]; I activate [impl,unit,int] per wave, same-commit tags). PR → ping doyle at PR-ready (self-drive until then). See [[v080-merge-reconciliation]] (the v0.8.0 livehost reapply this builds on).

**DEFECT (perri v0.8.0 dogfood, real bug):** harness-hosted live agent (`spt api seed` + `spt api listen <id>`) goes status=online but NEVER gets a daemon-hosted Psyche on a net-less/unpaired/peer-pump-STALLED node. Host-predicate fully passes (state=live_agent, status=online, adapter resolvable, psyche_init present) yet `reconcile_once` never emits `LIVEHOST_PSYCHE`. "peer pump: STALLED" reappears after every restart.

**VERIFIED from source (brainproc.rs:159-255):** run_brain :161 `connect_retry(&broker_socket)?` connects to the LOCAL BROKER (not net); broker binds its IPC listener INDEPENDENT of net (broker.rs:575 bind_in_with_net). So net-less the broker-connect succeeds → :230 `spawn_live_host` IS reached (unconditional, own thread, before the heartbeat loop). The heartbeat loop (:233) net-gates ONLY the consumers (dispatcher/peer pump) via consumer_gate, NOT livehost. ⇒ **H1 (brain starved before :230 by net) REFUTED by source** — the live reconcile thread runs net-less. (Caveat NOT yet verified: whether the BROKER itself spawns the brain child net-less, or the broker's own boot stalls before binding — H3/broker-side, open.)

**Explore report (afe7959) — TREAT WITH CAUTION, its mechanism is shaky:** concluded "registry load-order race → install_dir=None → spawn_psyche fails." INTERNALLY INCONSISTENT: `resolve_option_in(adapter)` and the install_dir `find` use the SAME `registered` slice + same parent — can't succeed-then-miss. And a first-tick registry miss self-heals next tick (5s) ≠ the PERSISTENT defect. So the specific theory is likely WRONG. doyle warned "do NOT presume (perri refuted doyle's first guess)."

**TRUE failure = runtime-observable only.** Real candidates: (a) `spawn_psyche` persistently fails → `LIVEHOST_PSYCHE_FAIL` every tick — most likely REQ-INSTALL-11 psyche-binary-not-found (perri's blocked proof, copy-mode binary absent from install_dir+PATH); (b) H3 broker doesn't spawn brain child net-less; (c) reconcile predicate fails on a field. doyle's brief: "check LIVEHOST_PSYCHE_FAIL."

**WAVE PLAN:**
- **W1 — reproduce via the non-negotiable gate artifact:** a REAL detached-daemon E2E (spawn the real brain CHILD via `spt daemon`, NOT in-process; existing live_firsthost_e2e uses in-process daemon + hand-writes perch, so the production broker→brain-child→run_brain→spawn_live_host→reconcile path was NEVER E2E-tested). `api seed` + `api listen` a live agent on a net-less/unpaired node → assert `LIVEHOST_PSYCHE`. Watch it FAIL → read the ACTUAL failure (LIVEHOST_PSYCHE_FAIL text / behavior). Ground the fix in THIS, not the report. (Look at crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs for real-brain-spawn infra.)
- **W2 — fix** per observed failure: live-host reconcile runs net-INDEPENDENTLY + survives the boot-race, self-healing like REQ-DAEMON-9 (dispatcher/peer-pump boot-race self-heal in broker.rs attach_net). Likely also fix REQ-INSTALL-11 psyche binary resolution if that's the observed failure.
- **W3 — green + activate + gate.**

**WHY IT MATTERS (doyle):** unblocks perri's last 2 parity proofs — REQ-INSTALL-11 install-dir binary resolution (psyche must actually SPAWN to exercise it) + F-006 interim PATH-copy.

**GATE (pre-flight before pinging doyle):** `clippy --workspace --all-targets -D warnings` (EXACT CI invocation, NOT -p) · `traceable-reqs check` EXIT=0 · docs-drift `xtask gen` if any CLI touched · the real-daemon E2E GREEN. Then doyle code-reads + verifies the E2E spawns the brain CHILD + gates → deployah ships v0.8.1 (counter 17) → doyle pings perri.

**LESSON IN FLIGHT:** an Explore subagent over-theorized a shaky root cause; ground in source (done for boot path) + RUNTIME repro (the E2E), not the agent's narrative. [[grep-req-tags-to-find-impl]] sibling lesson.


File created successfully at: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\v081-livehost-bootrace.md (file state is current in your context — no need to Read it back)

---
name: v081-livehost-bootrace
description: "v0.8.1 task — fix REQ-HAZARD-LIVEHOST-BOOT-RACE: net-less node, live agent goes online but daemon never hosts Psyche (no LIVEHOST_PSYCHE). doyle delegation, I own waves+execution. Verified root-cause-so-far + wave plan + the non-negotiable real-daemon E2E gate."
metadata:
  node_type: memory
  type: project
  originSessionId: 8350d43e-fe2d-4e24-9999-eecbcc6d3a50
---

**v0.8.1 livehost boot-race** — doyle delegation (operator-directed), I own W/T planning + execution, doyle gates + drives release (counter 17). Branch `v0.8.1-livehost-bootrace` off main (v0.8.0), base @2aab895 mints `REQ-HAZARD-LIVEHOST-BOOT-RACE` (required_stages=[]; I activate [impl,unit,int] per wave, same-commit tags). PR → ping doyle at PR-ready (self-drive until then). See [[v080-merge-reconciliation]] (the v0.8.0 livehost reapply this builds on).

**DEFECT (perri v0.8.0 dogfood, real bug):** harness-hosted live agent (`spt api seed` + `spt api listen <id>`) goes status=online but NEVER gets a daemon-hosted Psyche on a net-less/unpaired/peer-pump-STALLED node. Host-predicate fully passes (state=live_agent, status=online, adapter resolvable, psyche_init present) yet `reconcile_once` never emits `LIVEHOST_PSYCHE`. "peer pump: STALLED" reappears after every restart.

**VERIFIED from source (brainproc.rs:159-255):** run_brain :161 `connect_retry(&broker_socket)?` connects to the LOCAL BROKER (not net); broker binds its IPC listener INDEPENDENT of net (broker.rs:575 bind_in_with_net). So net-less the broker-connect succeeds → :230 `spawn_live_host` IS reached (unconditional, own thread, before the heartbeat loop). The heartbeat loop (:233) net-gates ONLY the consumers (dispatcher/peer pump) via consumer_gate, NOT livehost. ⇒ **H1 (brain starved before :230 by net) REFUTED by source** — the live reconcile thread runs net-less. (Caveat NOT yet verified: whether the BROKER itself spawns the brain child net-less, or the broker's own boot stalls before binding — H3/broker-side, open.)

**Explore report (afe7959) — TREAT WITH CAUTION, its mechanism is shaky:** concluded "registry load-order race → install_dir=None → spawn_psyche fails." INTERNALLY INCONSISTENT: `resolve_option_in(adapter)` and the install_dir `find` use the SAME `registered` slice + same parent — can't succeed-then-miss. And a first-tick registry miss self-heals next tick (5s) ≠ the PERSISTENT defect. So the specific theory is likely WRONG. doyle warned "do NOT presume (perri refuted doyle's first guess)."

**TRUE failure = runtime-observable only.** Real candidates: (a) `spawn_psyche` persistently fails → `LIVEHOST_PSYCHE_FAIL` every tick — most likely REQ-INSTALL-11 psyche-binary-not-found (perri's blocked proof, copy-mode binary absent from install_dir+PATH); (b) H3 broker doesn't spawn brain child net-less; (c) reconcile predicate fails on a field. doyle's brief: "check LIVEHOST_PSYCHE_FAIL."

**WAVE PLAN:**
- **W1 — reproduce via the non-negotiable gate artifact:** a REAL detached-daemon E2E (spawn the real brain CHILD via `spt daemon`, NOT in-process; existing live_firsthost_e2e uses in-process daemon + hand-writes perch, so the production broker→brain-child→run_brain→spawn_live_host→reconcile path was NEVER E2E-tested). `api seed` + `api listen` a live agent on a net-less/unpaired node → assert `LIVEHOST_PSYCHE`. Watch it FAIL → read the ACTUAL failure (LIVEHOST_PSYCHE_FAIL text / behavior). Ground the fix in THIS, not the report. (Look at crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs for real-brain-spawn infra.)
- **W2 — fix** per observed failure: live-host reconcile runs net-INDEPENDENTLY + survives the boot-race, self-healing like REQ-DAEMON-9 (dispatcher/peer-pump boot-race self-heal in broker.rs attach_net). Likely also fix REQ-INSTALL-11 psyche binary resolution if that's the observed failure.
- **W3 — green + activate + gate.**

**WHY IT MATTERS (doyle):** unblocks perri's last 2 parity proofs — REQ-INSTALL-11 install-dir binary resolution (psyche must actually SPAWN to exercise it) + F-006 interim PATH-copy.

**GATE (pre-flight before pinging doyle):** `clippy --workspace --all-targets -D warnings` (EXACT CI invocation, NOT -p) · `traceable-reqs check` EXIT=0 · docs-drift `xtask gen` if any CLI touched · the real-daemon E2E GREEN. Then doyle code-reads + verifies the E2E spawns the brain CHILD + gates → deployah ships v0.8.1 (counter 17) → doyle pings perri.

**LESSON IN FLIGHT:** an Explore subagent over-theorized a shaky root cause; ground in source (done for boot path) + RUNTIME repro (the E2E), not the agent's narrative. [[grep-req-tags-to-find-impl]] sibling lesson.


Adding the memory index pointer + reading the real-brain test infra (to ground the W1 E2E + check the open H3 broker-spawns-brain angle):


Edit

- [v0.8.0 merge reconciliation](v080-merge-reconciliation.md) — unified v0.8.0 merge NON-clean:

- [v0.8.1 livehost boot-race](v081-livehost-bootrace.md) — doyle delegation (I own waves+exec, doyle gates→counter 17): net-less node, live agent goes online but daemon never hosts Psyche (no LIVEHOST_PSYCHE). VERIFIED: spawn_live_host @brainproc:230 IS reached net-less (broker-connect is local) → H1 refuted. Explore report's "registry race" theory SHAKY (same-slice inconsistency). True failure runtime-only → W1 = real-detached-daemon E2E (non-negotiable gate) reproduces it. Likely REQ-INSTALL-11 psyche-binary-not-found. Branch v0.8.1-livehost-bootrace @2aab895
- [v0.8.0 merge reconciliation](v080-merge-reconciliation.md) — unified v0.8.0 merge NON-clean:


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

- [v0.8.1 livehost boot-race](v081-livehost-bootrace.md) — doyle delegation (I own waves+exec, doyle gates→counter 17): net-less node, live agent goes online but daemon never hosts Psyche (no LIVEHOST_PSYCHE). VERIFIED: spawn_live_host @brainproc:230 IS reached net-less (broker-connect is local) → H1 refuted. Explore report's "registry race" theory SHAKY (same-slice inconsistency). True failure runtime-only → W1 = real-detached-daemon E2E (non-negotiable gate) reproduces it. Likely REQ-INSTALL-11 psyche-binary-not-found. Branch v0.8.1-livehost-bootrace @2aab895
- [v0.8.0 merge reconciliation](v080-merge-reconciliation.md) — unified v0.8.0 merge NON-clean:

- [pre-flight clippy --workspace](ci-clippy-preflight-workspace.md) — CI denies warnings workspace-wide (`clippy --workspace --all-targets -D warnings`); a `-p <crate>` local check misses sibling-crate lints → CI red round-trip (M11-W1: `-p spt` missed a `useless_vec` in spt-daemon's act-gate test). Pre-flight the exact CI invocation. CI only fires on PR (push branches=main/dev-freeform)

+- [v0.8.1 livehost boot-race](v081-livehost-bootrace.md) — doyle delegation (I own waves+exec, doyle gates→counter 17): net-less node, live agent goes online but daemon never hosts Psyche (no LIVEHOST_PSYCHE). VERIFIED: spawn_live_host @brainproc:230 IS reached net-less (broker-connect is local) → H1 refuted. Explore report's "registry race" theory SHAKY (same-slice inconsistency). True failure runtime-only → W1 = real-detached-daemon E2E (non-negotiable gate) reproduces it. Likely REQ-INSTALL-11 psyche-binary-not-found. Branch v0.8.1-livehost-bootrace @2aab895

- [v0.8.0 merge reconciliation](v080-merge-reconciliation.md) — unified v0.8.0 merge NON-clean: live psyche model diverged (M11 daemon-host WINS vs main Feature B in-process install-dir spawn); naive merge silently DROPS Feature B. Reapply recipe (livehost.rs:191 → `with_config_in` w/ registry `source_dir`) + traceable re-tag caveat. BLOCKED: main Feature B on doyle's local unpushed main; wait for push + merge-ordering ping. how-to-live shipped @672b928

- [F-007 live-relay acceptance ruling](f007-live-relay-acceptance.md) — live bringup = persistent `api listen <id>` via Monitor (heir to legacy $LIVE start), NOT `--once` (no-Monitor degenerate) nor $OWL listen (ready); psyche-spawn gated on `--manifest` (declares psyche_init, fires regardless of --once) + the v0.7.4 install-dir binary fix (REQ-INSTALL-11) for bare psyche binaries; both F-007 items Bucket 2 docs gap. Lesson: ground in CONTEXT.md before ruling (I erred recommending --once)

- [delegate-build preflight hang](delegate-build-preflight-hang.md) — background build agents hang silently on the full spt-daemon/spt integration suite in preflight (no completion notification on hang); instruct targeted units only (`--lib` + named tests); check liveness via transcript mtime, TaskStop the stalled one, take ownership of the (usually good) partial work + verify


1	//! M11-W0.4 (i) production-path int (REQ-DAEMON-1) — re-points the
2	//! `[int->REQ-DAEMON-1]` evidence OFF the type-level `daemon_e2e` (which
3	//! constructs a [`BrainLifecycle`] directly — green coverage over an UNWIRED
4	//! production path, the M3-era false-green) ONTO the actual brain entry point:
5	//!
6	//! a [`Broker`] + `brainproc::run_brain` (the real brain's `main`-equivalent,
7	//! which since M11-W0.1 runs `livehost::spawn_live_host`) HOSTS the Psyche for a
8	//! real ONLINE live endpoint, and the brain's pulse loop ingests a commune. The
9	//! reconcile that hosts is the REAL brain's, not an in-test `reconcile_once`
10	//! call. A cold `run_brain` over an already-online perch IS the post-brain-restart
11	//! rehost: the perch's `status=online` survives the brain process, and the fresh
12	//! brain re-hosts the lifecycle at boot (daemon-authoritative liveness,
13	//! CONTEXT:194).
14	//!
15	//! Single `#[test]`: it owns the process-global `SPT_HOME`.
16	// [int->REQ-DAEMON-1]
17
18	use std::sync::Arc;
19	use std::thread;
20	use std::time::{Duration, Instant};
21
22	use spt_daemon::brainproc::{run_brain, StartReason};
23	use spt_daemon::endpoint::broker_socket_name;
24	use spt_daemon::Broker;
25	use spt_store::liveness::STATUS_ONLINE;
26	use spt_store::perch::{self, ParentHint};
27
28	// A no-op Psyche summarizer: spawns + exits 0 (the daemon online-stamp hosts it).
29	#[cfg(windows)]
30	const NOOP: &str = "cmd /C rem";
31	#[cfg(unix)]
32	const NOOP: &str = "true";
33
34	#[test]
35	fn real_brain_process_hosts_the_psyche_for_an_online_live_endpoint() {
36	    let home = tempfile::tempdir().unwrap();
37	    std::env::set_var("SPT_HOME", home.path());
38
39	    // A live-capable adapter, registered so the brain resolves the manifest from
40	    // disk by the perch's adapter (the production resolve path).
41	    let drops = home.path().join("drops");
42	    std::fs::create_dir_all(&drops).unwrap();
43	    let dir = drops.to_string_lossy().replace('\\', "/");
44	    let manifest = format!(
45	        "[adapter]\nname=\"mock\"\nkind=\"harness\"\nversion=\"1\"\n\
46	         min_spt_core_version=\"0\"\n\n\
47	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
48	         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
49	    );
50	    let src = perch::spt_home().join("srcs").join("mock");
51	    std::fs::create_dir_all(&src).unwrap();
52	    std::fs::write(src.join("manifest.toml"), &manifest).unwrap();
53	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
54
55	    // An ONLINE live endpoint on disk — as a real `api bind/listen` left it (the
56	    // establish→online path is proven by live_firsthost/live_bind ints; here the
57	    // focus is the REAL brain hosting it). status=online survives the brain.
58	    let id = "agent9";
59	    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
60	    std::fs::create_dir_all(&perch_path).unwrap();
61	    let mut rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-9", "live_agent");
62	    rec.adapter = Some("mock".to_string());
63	    spt_store::info::write_info(&perch_path, &rec).unwrap();
64	    spt_store::info::set_status(&perch_path, STATUS_ONLINE).unwrap();
65
66	    // ── the REAL brain: a Broker + brainproc::run_brain on a thread (NOT a direct
67	    // BrainLifecycle::with_config). run_brain runs spawn_live_host, so the reconcile
68	    // hosting the Psyche is the real brain's, at boot.
69	    let broker = Broker::bind(&broker_socket_name()).expect("bind broker");
70	    {
71	        let serve = Arc::clone(&broker);
72	        thread::spawn(move || {
73	            let _ = serve.serve();
74	        });
75	    }
76	    thread::spawn(|| {
77	        let _ = run_brain(0, StartReason::Cold);
78	    });
79
80	    // The REAL brain process hosts the Psyche: its boot reconcile spawns the Psyche
81	    // nested perch (= the post-restart rehost — a cold brain over an online perch).
82	    let psyche_perch = perch::resolve_perch_path("agent9-psyche", ParentHint::Explicit("agent9"));
83	    let deadline = Instant::now() + Duration::from_secs(25);
84	    loop {
85	        if spt_store::info::read_info(&psyche_perch).is_some() {
86	            break;
87	        }
88	        assert!(
89	            Instant::now() < deadline,
90	            "the REAL brain process (run_brain) must host the Psyche for the online endpoint"

//! M11-W0.4 (i) production-path int (REQ-DAEMON-1) — re-points the
//! `[int->REQ-DAEMON-1]` evidence OFF the type-level `daemon_e2e` (which
//! constructs a [`BrainLifecycle`] directly — green coverage over an UNWIRED
//! production path, the M3-era false-green) ONTO the actual brain entry point:
//!
//! a [`Broker`] + `brainproc::run_brain` (the real brain's `main`-equivalent,
//! which since M11-W0.1 runs `livehost::spawn_live_host`) HOSTS the Psyche for a
//! real ONLINE live endpoint, and the brain's pulse loop ingests a commune. The
//! reconcile that hosts is the REAL brain's, not an in-test `reconcile_once`
//! call. A cold `run_brain` over an already-online perch IS the post-brain-restart
//! rehost: the perch's `status=online` survives the brain process, and the fresh
//! brain re-hosts the lifecycle at boot (daemon-authoritative liveness,
//! CONTEXT:194).
//!
//! Single `#[test]`: it owns the process-global `SPT_HOME`.
// [int->REQ-DAEMON-1]

use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use spt_daemon::brainproc::{run_brain, StartReason};
use spt_daemon::endpoint::broker_socket_name;
use spt_daemon::Broker;
use spt_store::liveness::STATUS_ONLINE;
use spt_store::perch::{self, ParentHint};

// A no-op Psyche summarizer: spawns + exits 0 (the daemon online-stamp hosts it).
#[cfg(windows)]
const NOOP: &str = "cmd /C rem";
#[cfg(unix)]
const NOOP: &str = "true";

#[test]
fn real_brain_process_hosts_the_psyche_for_an_online_live_endpoint() {
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    // A live-capable adapter, registered so the brain resolves the manifest from
    // disk by the perch's adapter (the production resolve path).
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest = format!(
        "[adapter]\nname=\"mock\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    let src = perch::spt_home().join("srcs").join("mock");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), &manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // An ONLINE live endpoint on disk — as a real `api bind/listen` left it (the
    // establish→online path is proven by live_firsthost/live_bind ints; here the
    // focus is the REAL brain hosting it). status=online survives the brain.
    let id = "agent9";
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    let mut rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-9", "live_agent");
    rec.adapter = Some("mock".to_string());
    spt_store::info::write_info(&perch_path, &rec).unwrap();
    spt_store::info::set_status(&perch_path, STATUS_ONLINE).unwrap();

    // ── the REAL brain: a Broker + brainproc::run_brain on a thread (NOT a direct
    // BrainLifecycle::with_config). run_brain runs spawn_live_host, so the reconcile
    // hosting the Psyche is the real brain's, at boot.
    let broker = Broker::bind(&broker_socket_name()).expect("bind broker");
    {
        let serve = Arc::clone(&broker);
        thread::spawn(move || {
            let _ = serve.serve();
        });
    }
    thread::spawn(|| {
        let _ = run_brain(0, StartReason::Cold);
    });

    // The REAL brain process hosts the Psyche: its boot reconcile spawns the Psyche
    // nested perch (= the post-restart rehost — a cold brain over an online perch).
    let psyche_perch = perch::resolve_perch_path("agent9-psyche", ParentHint::Explicit("agent9"));
    let deadline = Instant::now() + Duration::from_secs(25);
    loop {
        if spt_store::info::read_info(&psyche_perch).is_some() {
            break;
        }
        assert!(
            Instant::now() < deadline,
            "the REAL brain process (run_brain) must host the Psyche for the online endpoint"


The existing `daemon_lifecycle_real_brain.rs` runs `run_brain` **on a thread** (not a child process), hand-writes the online perch, and uses a **NOOP** psyche (`cmd /C rem` — a builtin, always resolves) — yet it PASSES. That's decisive: the reconcile predicate logic works when the binary resolves. So the bug needs a **real psyche binary** (REQ-INSTALL-11 resolution) and/or the **real detached brain-child** + real `api seed`/`listen` — exactly the seam doyle says was never tested. Strongly points to H2.

Let me verify how the real daemon spawns the brain as a child process (determines the E2E shape) before I spec W1:


crates\spt-daemon\src\access.rs:35://! an `spt access` edit takes effect on the next inbound without a daemon
crates\spt-daemon\src\brain.rs:982:    /// Q3): the broker signals its supervisor to cycle the brain child onto the
crates\spt-daemon\src\applyhost.rs:85:/// swap (production: `std::env::current_exe()`); `broker_name` the canonical
crates\spt-daemon\src\applyhost.rs:228:    // The broker signals its supervisor to cycle the brain child onto the
crates\spt-daemon\src\applyhost.rs:309:        format!("spt-daemon-d7b-{}-{}.sock", std::process::id(), n)
crates\spt-daemon\src\applyhost.rs:594:        drop(seat); // the seat brain detaches; the broker keeps the child
crates\spt-daemon\src\config.rs:8://! the spt home (`<spt_home>/daemon.json`) and falling back to the default when
crates\spt-daemon\src\config.rs:66:/// The on-disk shape (`<spt_home>/daemon.json`). Every field is optional so a
crates\spt-daemon\src\config.rs:181:    /// The canonical config path: `<spt_home>/daemon.json`.
crates\spt-daemon\src\config.rs:183:        spt_store::perch::spt_home().join("daemon.json")
crates\spt-daemon\src\broker.rs:29://! brain disconnect only detaches the subscriber; the child, its PTY, and the
crates\spt-daemon\src\broker.rs:534:    /// effect journal at the default path (`<spt_home>/daemon-effects.log`).
crates\spt-daemon\src\broker.rs:670:    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and
crates\spt-daemon\src\broker.rs:1079:    /// supervisor's restart signal so it cycles the brain child onto the swapped
crates\spt-daemon\src\broker.rs:1385:/// The default broker-owned effect-journal path: `<spt_home>/daemon-effects.log`.
crates\spt-daemon\src\broker.rs:1387:    spt_store::perch::spt_home().join("daemon-effects.log")
crates\spt-daemon\src\deadline.rs:4://! (alarms) must survive a brain swap: the supervisor respawns a fresh brain
crates\spt-daemon\src\brainproc.rs:8://! skeleton*: `spt daemon run` is the broker process and spawns a supervised
crates\spt-daemon\src\brainproc.rs:9://! `spt daemon brain` child. The child connects over the existing broker socket
crates\spt-daemon\src\brainproc.rs:19://! broker spawns + supervises a brain child over the socket, and the broker
crates\spt-daemon\src\brainproc.rs:59:/// How long the brain child retries connecting to the broker socket before
crates\spt-daemon\src\brainproc.rs:65:/// Why the broker spawned this brain — the Q4 update-vs-crash discriminator the
crates\spt-daemon\src\brainproc.rs:78:    /// A supervised respawn after an unexpected brain exit.
crates\spt-daemon\src\brainproc.rs:85:    /// The argv token the broker passes to `spt daemon brain --start-reason`.
crates\spt-daemon\src\brainproc.rs:108:/// its next wait tick, kills the current brain child, and respawns it with
crates\spt-daemon\src\brainproc.rs:138:/// The `spt daemon brain` child entry. Connect to the broker over the existing
crates\spt-daemon\src\brainproc.rs:159:pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {
crates\spt-daemon\src\brainproc.rs:210:    // the broker process to the brain child: shellwake is brain-owned logic
crates\spt-daemon\src\brainproc.rs:268:/// the brain child (restoration D2-1):** both are standalone IPC clients
crates\spt-daemon\src\brainproc.rs:272:/// it, and the respawned brain re-spawns them from the current binary. Mirrors

[Showing results with pagination = limit: 30]


Now I have the production shape: `spt daemon run` (broker) spawns a supervised `spt daemon brain` **child process** via `current_exe`. That's the detached path doyle wants exercised — the existing real-brain test runs `run_brain` on a *thread* with a NOOP psyche, closing none of the three gaps.

Given my session depth, I'll delegate the W1 E2E build + reproduction to a fresh subagent (well-scoped, full budget), keeping the fix decision (W2) myself. Tight spec grounded in what I verified, bounded runs only (per the preflight-hang lesson):


Agent

Build real-daemon E2E, reproduce bug

Prompt:
Build a NEW integration test in the spt-core Rust workspace (cwd C:\Users\decid\Documents\projects\spt-core, branch v0.8.1-livehost-bootrace) that REPRODUCES a daemon boot-race bug, then REPORT the observed failure. Do NOT fix the bug. Do NOT run the full test suite (it hangs in preflight) — compile and run ONLY your new test, with a bounded deadline so a reproduced failure is a clean timeout/assert, never a hang.

THE BUG: a harness-hosted live agent goes status=online but the daemon never hosts its Psyche (no `LIVEHOST_PSYCHE` log) on a net-less/unpaired node. doyle's mandate: the real-daemon E2E MUST spawn the REAL brain CHILD PROCESS (not in-process), via the real `api seed` + `api listen` path, exercising a REAL psyche binary resolved from the adapter install dir (REQ-INSTALL-11) — NOT a NOOP shell builtin.

STUDY FIRST (read, cite file:line in your report):
- crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs — the CLOSEST analog, but it has 3 gaps your test must close: (1) it runs `run_brain` on a THREAD, not a detached child process; (2) it uses NOOP psyche `cmd /C rem` (a builtin, always resolves — never exercises install-dir binary resolution); (3) it HAND-WRITES the online perch instead of going through real `api seed`+`api listen`.
- crates/spt-daemon/src/brainproc.rs — `spt daemon run` is the broker process and spawns a supervised `spt daemon brain` CHILD (see the module doc ~:8 and run_brain :159). Find the EXACT `spt daemon ...` CLI invocation (grep the spt binary crate, e.g. crates/spt/src for the daemon subcommand parsing) so your test launches the real daemon via CARGO_BIN_EXE_spt.
- crates/spt-daemon/src/livehost.rs — reconcile_once/host_one (LIVEHOST_PSYCHE at ~:212/225, LIVEHOST_PSYCHE_FAIL at ~:227). spawn_live_host runs on its own thread at brainproc.rs:230.
- crates/spt/tests/live_firsthost_e2e.rs and contract_e2e.rs / other crates/spt/tests for the established pattern of: launching the spt binary, `api seed`, `api listen` as a child capturing stdout/stderr, isolated SPT_HOME tempdir, pinning OWL_SESSION_ID, deterministic identity isolation.

THE TEST TO BUILD (put it in crates/spt/tests/ or crates/spt-daemon/tests/ — your call based on where CARGO_BIN_EXE_spt + the daemon entry are reachable):
1. Isolated SPT_HOME tempdir, net-less / UNPAIRED (no subnet created, no pairing) — the defect condition.
2. Register a live-capable adapter in COPY mode whose `[session.psyche_init].command` is a BARE program token naming a real psyche binary that ships in the adapter's install dir (source_dir) — so spawn_psyche must resolve it via REQ-INSTALL-11 install-dir resolution, NOT PATH. For the psyche binary fixture: a tiny real executable that just sleeps/exits 0. Simplest portable approach: build a trivial helper (a small script made executable, or reuse an existing built test-binary/example in the repo, or a `cmd`/`sh` wrapper file placed IN the install dir and referenced by bare name). Investigate what the repo already provides (e.g. adapters/mock, any example bins) and pick the most robust cross-platform option. The KEY property: the psyche command's program token is a BARE name present in the install dir and NOT on PATH, so it ONLY resolves if install-dir resolution works.
3. Launch the REAL daemon as a detached child process (`spt daemon run` or whatever the real entry is) via CARGO_BIN_EXE_spt with the tempdir SPT_HOME — so it spawns a real `spt daemon brain` child.
4. `spt api ... seed` then `spt api ... listen <id>` a live agent (the real establish→online path), as the harness would. Capture the brain child's stderr if reachable (it may log to a file under SPT_HOME — investigate where the daemon/brain stderr goes; LIVEHOST_PSYCHE / LIVEHOST_PSYCHE_FAIL are eprintln! to the brain's stderr).
5. Assert `LIVEHOST_PSYCHE:<id>` appears (the psyche nested perch `<id>-psyche` coming online is the disk-observable proxy — mirror daemon_lifecycle_real_brain.rs's psyche-perch poll) within a BOUNDED deadline (~25s). On the buggy tree this SHOULD FAIL.
6. Reap any spawned daemon/brain processes at the end (scope kill to the PIDs you spawned or the tempdir — NEVER a machine-wide `Get-Process spt | Stop-Process`, a live broker shares this box).

RUN IT (bounded, targeted only): `cargo test -p <crate> --test <your_test_name> -- --nocapture` (single test, never the whole suite). If compilation pulls the whole suite, still target just your test binary.

REPORT (this is the deliverable):
- The exact path/name of the test you created and its full final source.
- Whether it REPRODUCES the bug (fails on this tree) — and the EXACT observed failure: the verbatim `LIVEHOST_PSYCHE_FAIL:<id>: <error>` text if present, or the timeout behavior, or whatever actually happened (psyche perch never appears? brain child not spawned? error in brain stderr?). Quote real captured output.
- Where the brain/daemon child writes its stderr/logs (so the fix author can see markers).
- Your evidence-grounded root cause: WHY net-less the Psyche isn't hosted, with file:line. Specifically confirm or refute: does spawn_psyche fail on install-dir binary resolution (REQ-INSTALL-11)? Is the brain child even spawned net-less? Does the reconcile run?
- Candidate fix location(s) with file:line — but do NOT implement the fix.

Be rigorous and ground every claim in real output or source — do not presume. If you cannot get the real daemon to spawn a brain child in a test, report exactly what blocked you (that itself is a finding).


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Async agent launched successfully.
agentId: af5fc4ee430d30dbd (internal ID - do not mention to user. Use SendMessage with to: 'af5fc4ee430d30dbd' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\af5fc4ee430d30dbd.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Build real-daemon E2E, reproduce bug

Prompt:
Build a NEW integration test in the spt-core Rust workspace (cwd C:\Users\decid\Documents\projects\spt-core, branch v0.8.1-livehost-bootrace) that REPRODUCES a daemon boot-race bug, then REPORT the observed failure. Do NOT fix the bug. Do NOT run the full test suite (it hangs in preflight) — compile and run ONLY your new test, with a bounded deadline so a reproduced failure is a clean timeout/assert, never a hang.

THE BUG: a harness-hosted live agent goes status=online but the daemon never hosts its Psyche (no `LIVEHOST_PSYCHE` log) on a net-less/unpaired node. doyle's mandate: the real-daemon E2E MUST spawn the REAL brain CHILD PROCESS (not in-process), via the real `api seed` + `api listen` path, exercising a REAL psyche binary resolved from the adapter install dir (REQ-INSTALL-11) — NOT a NOOP shell builtin.

STUDY FIRST (read, cite file:line in your report):
- crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs — the CLOSEST analog, but it has 3 gaps your test must close: (1) it runs `run_brain` on a THREAD, not a detached child process; (2) it uses NOOP psyche `cmd /C rem` (a builtin, always resolves — never exercises install-dir binary resolution); (3) it HAND-WRITES the online perch instead of going through real `api seed`+`api listen`.
- crates/spt-daemon/src/brainproc.rs — `spt daemon run` is the broker process and spawns a supervised `spt daemon brain` CHILD (see the module doc ~:8 and run_brain :159). Find the EXACT `spt daemon ...` CLI invocation (grep the spt binary crate, e.g. crates/spt/src for the daemon subcommand parsing) so your test launches the real daemon via CARGO_BIN_EXE_spt.
- crates/spt-daemon/src/livehost.rs — reconcile_once/host_one (LIVEHOST_PSYCHE at ~:212/225, LIVEHOST_PSYCHE_FAIL at ~:227). spawn_live_host runs on its own thread at brainproc.rs:230.
- crates/spt/tests/live_firsthost_e2e.rs and contract_e2e.rs / other crates/spt/tests for the established pattern of: launching the spt binary, `api seed`, `api listen` as a child capturing stdout/stderr, isolated SPT_HOME tempdir, pinning OWL_SESSION_ID, deterministic identity isolation.

THE TEST TO BUILD (put it in crates/spt/tests/ or crates/spt-daemon/tests/ — your call based on where CARGO_BIN_EXE_spt + the daemon entry are reachable):
1. Isolated SPT_HOME tempdir, net-less / UNPAIRED (no subnet created, no pairing) — the defect condition.
2. Register a live-capable adapter in COPY mode whose `[session.psyche_init].command` is a BARE program token naming a real psyche binary that ships in the adapter's install dir (source_dir) — so spawn_psyche must resolve it via REQ-INSTALL-11 install-dir resolution, NOT PATH. For the psyche binary fixture: a tiny real executable that just sleeps/exits 0. Simplest portable approach: build a trivial helper (a small script made executable, or reuse an existing built test-binary/example in the repo, or a `cmd`/`sh` wrapper file placed IN the install dir and referenced by bare name). Investigate what the repo already provides (e.g. adapters/mock, any example bins) and pick the most robust cross-platform option. The KEY property: the psyche command's program token is a BARE name present in the install dir and NOT on PATH, so it ONLY resolves if install-dir resolution works.
3. Launch the REAL daemon as a detached child process (`spt daemon run` or whatever the real entry is) via CARGO_BIN_EXE_spt with the tempdir SPT_HOME — so it spawns a real `spt daemon brain` child.
4. `spt api ... seed` then `spt api ... listen <id>` a live agent (the real establish→online path), as the harness would. Capture the brain child's stderr if reachable (it may log to a file under SPT_HOME — investigate where the daemon/brain stderr goes; LIVEHOST_PSYCHE / LIVEHOST_PSYCHE_FAIL are eprintln! to the brain's stderr).
5. Assert `LIVEHOST_PSYCHE:<id>` appears (the psyche nested perch `<id>-psyche` coming online is the disk-observable proxy — mirror daemon_lifecycle_real_brain.rs's psyche-perch poll) within a BOUNDED deadline (~25s). On the buggy tree this SHOULD FAIL.
6. Reap any spawned daemon/brain processes at the end (scope kill to the PIDs you spawned or the tempdir — NEVER a machine-wide `Get-Process spt | Stop-Process`, a live broker shares this box).

RUN IT (bounded, targeted only): `cargo test -p <crate> --test <your_test_name> -- --nocapture` (single test, never the whole suite). If compilation pulls the whole suite, still target just your test binary.

REPORT (this is the deliverable):
- The exact path/name of the test you created and its full final source.
- Whether it REPRODUCES the bug (fails on this tree) — and the EXACT observed failure: the verbatim `LIVEHOST_PSYCHE_FAIL:<id>: <error>` text if present, or the timeout behavior, or whatever actually happened (psyche perch never appears? brain child not spawned? error in brain stderr?). Quote real captured output.
- Where the brain/daemon child writes its stderr/logs (so the fix author can see markers).
- Your evidence-grounded root cause: WHY net-less the Psyche isn't hosted, with file:line. Specifically confirm or refute: does spawn_psyche fail on install-dir binary resolution (REQ-INSTALL-11)? Is the brain child even spawned net-less? Does the reconcile run?
- Candidate fix location(s) with file:line — but do NOT implement the fix.

Be rigorous and ground every claim in real output or source — do not presume. If you cannot get the real daemon to spawn a brain child in a test, report exactly what blocked you (that itself is a finding).


W1 reproduction agent launched (`af5fc4ee`, background) — building the real-detached-daemon E2E (real brain child + real psyche binary in install_dir + real `api seed`/`listen`, net-less), running it bounded, reporting the observed failure + root cause + fix locations. Won't fix — that's my call once I see the real failure.

**State** (durable in memory [[v081-livehost-bootrace]]):
- v0.8.1 livehost boot-race, branch @2aab895. Verified from source: `spawn_live_host` IS reached net-less (broker-connect is local) → **H1 refuted**. The Explore report's "registry race" theory is shaky (same-slice inconsistency). Existing real-brain test passes with NOOP psyche → predicate logic works; bug needs a **real psyche binary** (REQ-INSTALL-11) + the real detached path → strongly **H2**.
- W1 (reproduce via the non-negotiable real-daemon E2E gate) in flight; W2 = fix from observed reality (net-independent reconcile / install-dir resolution, self-heal like REQ-DAEMON-9); W3 = green + gate.
- Gate: `clippy --workspace --all-targets -D warnings` · traceable EXIT0 · docs-drift if CLI touched · E2E green. Then ping doyle.

Holding for the reproduction report.


<task-notification>
<task-id>af5fc4ee430d30dbd</task-id>
<tool-use-id>toolu_01Gnn4F1BeTzRgU6BygynFMb</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\30df62c3-0b22-4b14-88de-fef422a5554f\tasks\af5fc4ee430d30dbd.output</output-file>
<status>completed</status>
<summary>Agent "Build real-daemon E2E, reproduce bug" completed</summary>
<result>## Report

### Test created
`C:\Users\decid\Documents\projects\spt-core\crates\spt\tests\livehost_bootrace_e2e.rs` — test fn `netless_online_live_agent_is_hosted_by_the_real_daemon_brain`, tagged `[int->REQ-DAEMON-1]`.

It closes the production-path E2E seam the hazard names: launches the REAL `spt daemon run` broker process (which spawns a supervised real `spt daemon brain` child), forces the truest net-less/unpaired condition (corrupt `node.key` → `NODE_KEY_FAIL`, no net, no retry), drives the REAL `api seed` + persistent `api listen` establish→online path, uses a REAL psyche binary (`psychebin`, a copy of `spt`) referenced by a BARE token present ONLY in the adapter install dir (so REQ-INSTALL-11 install-dir resolution is the only way it resolves), and asserts the nested `<id>-psyche` perch appears within a bounded 25s poll. All spawned PIDs (broker, supervised brain, listen) are reaped scoped to our PIDs only. Compiles clean; runs in ~8s.

### Does it reproduce the bug? NO — the bug does NOT reproduce on this tree.
The test PASSES. Verbatim captured brain stderr on the clean run (net-less):
```
NODE_KEY_FAIL: identity unavailable — broker runs net-less, no retry
BRAIN_UP: connected to broker spt-daemon-broker-...sock (pid 176800, generation 0, start-reason cold)
LIVEHOST_PSYCHE:bootrace-agent pid=180532
```
The psyche perch `info.json` appears. Even with net fully starved (no `BRAIN_NET_CONSUMERS_UP` ever fires), the Psyche is hosted.

### Where brain/daemon stderr goes
The brain child sets no stdio (`spawn_brain_child`, `crates/spt-daemon/src/brainproc.rs:927-948`), so it inherits the broker's stderr. `LIVEHOST_PSYCHE` / `LIVEHOST_PSYCHE_FAIL` are `eprintln!` (`livehost.rs:225,227`) → land on the broker process's stderr. In production that's the foreground `spt daemon run`'s stderr (invisible to a harness that only sees `api listen`'s stdout). My test redirects the broker child's stderr to `<SPT_HOME>/brain.stderr.log` to capture them.

### Root cause analysis (evidence-grounded)
- **Net does NOT gate the Psyche host.** `livehost::spawn_live_host` is called UNCONDITIONALLY at `crates/spt-daemon/src/brainproc.rs:230`, BEFORE the heartbeat loop (lines 233-254) where the net-status `consumer_gate` lives. Net-less only suppresses the dispatcher/peer-pump (`BRAIN_NET_CONSUMERS_UP`), never the psyche reconcile. So on this branch the hazard's "net-attach starves the brain from reaching line 230" condition does not hold — line 230 runs regardless. **Refuted: net-gating is not the cause on this tree.**
- **The reconcile runs net-less and reaches `spawn_psyche`** — proven by a negative-control run (psyche binary removed): the brain logged `LIVEHOST_PSYCHE_FAIL:bootrace-agent: psyche spawn failed: failed to spawn session: program not found` once per 5s reconcile tick, net-less. So `reconcile_once` (`livehost.rs:137`) → `host_one` (`:216`) → `spawn_psyche` (`:224`) all run net-less; `resolve_option_in` (`:187`) and install-dir resolution (`:191-203`) execute.
- **The actual silent-failure mode is install-dir resolution, NOT net.** When the bare psyche token does NOT resolve in the install `source_dir` and is not on PATH, `spawn_session` returns `program not found`, `spawn_psyche` returns Err, and `host_one` logs `LIVEHOST_PSYCHE_FAIL` to brain stderr and returns — leaving the agent `status=online`/`alive=true` (stamped unconditionally by `cmd_listen`, `crates/spt/src/api/startup.rs:284`) with NO Psyche. That is exactly perri's diagnostic: "host-predicate fully passes yet reconcile_once never produced a Psyche."

So: the test seam itself (the deliverable the hazard explicitly calls "part of this hazard") is now closed, and it demonstrates the production net-less boot path correctly hosts the Psyche on this branch. The remaining live risk is install-dir resolution of the real adapter's psyche binary (REQ-INSTALL-11), whose failure is silent to the harness.

### Candidate fix locations (NOT implemented)
1. `crates/spt-daemon/src/livehost.rs:226-229` — `LIVEHOST_PSYCHE_FAIL` is `eprintln!`-only on the brain's (invisible) stderr. Surface a host failure to a disk-observable place keyed on the live perch so a harness can see "online but un-hosted" (the silent-failure masking the hazard describes).
2. `crates/spt-daemon/src/livehost.rs:197-203` — the install-dir resolution uses `rec.source_dir`. For a `--release`/`--github`-acquired adapter the shipped binary lands under `adapters/_github/<safe>/`, not `source_dir` (per `crates/spt-runtime/src/runtime.rs:419-426`). Verify the daemon-host site passes the SAME dir as the `api` seam (`resolve_ctx_manifest`, `crates/spt/src/api/mod.rs:480-507`) for those modes — a divergence here would be the real perri failure (binary present, but resolved against the wrong dir).
3. If the team still wants the net-independence guard the hazard names: the unconditional call at `brainproc.rs:230` already provides it on this tree; a regression test pinning "spawn_live_host is reached before any net gate" would lock it in.

I did not run the full suite (only this targeted test binary, as instructed). To reproduce: `cargo test -p spt --test livehost_bootrace_e2e -- --nocapture`.</result>
<usage><subagent_tokens>166386</subagent_tokens><tool_uses>58</tool_uses><duration_ms>484259</duration_ms></usage>
</task-notification>


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


<owl_messages>
<owl_message from="todlando-w160" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: todlando-w160
Agent type: general-purpose
To message it: $OWL send todlando-w160 todlando</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.

<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>


The W1 agent did rigorous, grounded work — with a negative control. Decisive findings (runtime-proven, not theory):

- **Real-daemon E2E PASSES on this tree** — net-less, the Psyche IS hosted (`LIVEHOST_PSYCHE` fires even with net fully starved). **H1 net-gating REFUTED by runtime** (confirms my source read: `spawn_live_host` @:230 is unconditional, before the heartbeat net-gate).
- **Negative control** (psyche binary removed): `LIVEHOST_PSYCHE_FAIL: ... program not found` once per 5s tick, net-less → reconcile runs, reaches `spawn_psyche`, install-dir resolution executes.
- **Real failure = install-dir binary resolution (REQ-INSTALL-11), NOT net.** Bare psyche token unresolved in install dir + not on PATH → silent `LIVEHOST_PSYCHE_FAIL` on the brain's invisible stderr → agent stays online/alive with no Psyche. Exactly perri's diagnostic.

This **reframes the root cause** away from doyle's net-boot-race hypothesis. The agent flagged but did NOT confirm the key suspect: for `--release`/`--github` adapters the binary lands under `adapters/_github/<safe>/`, but livehost uses `rec.source_dir`. I must verify whether that diverges before building a fix (doyle: "do NOT presume"). Both livehost and the api seam use `source_dir` — so the real question is what `source_dir` IS for github/release mode:


405	/// lines that live *outside* the role set, so [`ManifestRuntime::run_bounded`]
406	/// (which looks a role up in the manifest) does not apply. `stdin` feeds a
407	/// `normalize_command` the raw transcript on its standard input; it is written
408	/// on a thread so a child that reads slowly never deadlocks the poll.
409	// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]
410	pub fn run_bounded_command(
411	    template: &str,
412	    keys: &BTreeMap<String, String>,
413	    timeout: Duration,
414	    stdin: Option<&[u8]>,
415	) -> Result<BoundedOutput, RuntimeError> {
416	    run_bounded_command_in(template, keys, timeout, stdin, None)
417	}
418
419	/// Resolve a command template's **program token** (first whitespace-split token)
420	/// against an adapter's install dir before falling back to `PATH`. A `.spt`
421	/// adapter ships its built binaries either to `adapters/_github/<safe>/` (via
422	/// `--release`/`--github` acquisition) or keeps them in the adapter's
423	/// `source_dir` under copy-mode — both are the adapter's `AdapterRecord`
424	/// `source_dir`. A bare program name (`claude-spt-digest`) would otherwise miss
425	/// the shipped binary and resolve only from `PATH`; resolving against the install
426	/// dir first makes the adapter self-contained.
427	///
428	/// Returns `<install_dir>/<program>` (absolute) when that file exists — on
429	/// Windows also trying the `.exe` suffix — else the original bare token
430	/// unchanged (the `PATH` fallback). Pure and side-effect-free apart from the
431	/// existence probe; never errors (an unresolvable token is simply passed through
432	/// for the OS to resolve / fail at spawn).
433	// [impl->REQ-INSTALL-11]
434	pub fn resolve_program_in_dir(program: &str, install_dir: &Path) -> String {
435	    let direct = install_dir.join(program);
436	    if direct.is_file() {
437	        return direct.to_string_lossy().into_owned();
438	    }
439	    #[cfg(windows)]
440	    {
441	        if Path::new(program).extension().is_none() {
442	            let exe = install_dir.join(format!("{program}.exe"));
443	            if exe.is_file() {
444	                return exe.to_string_lossy().into_owned();
445	            }
446	        }
447	    }
448	    program.to_string()
449	}
450
451	/// As [`run_bounded_command`], but when `install_dir` is `Some`, the template's
452	/// program token is first resolved against that adapter install dir
453	/// ([`resolve_program_in_dir`]) so a bare program name binds to the adapter's
454	/// shipped binary before `PATH` (REQ-INSTALL-11). `None` preserves the legacy
455	/// PATH-only resolution for callers without an install dir.
456	// [impl->REQ-INSTALL-11]
457	// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]
458	pub fn run_bounded_command_in(
459	    template: &str,
460	    keys: &BTreeMap<String, String>,
461	    timeout: Duration,
462	    stdin: Option<&[u8]>,
463	    install_dir: Option<&Path>,
464	) -> Result<BoundedOutput, RuntimeError> {
465	    let filled = fill_template(template, keys)?;
466	    let tokens = tokenize(&filled);
467	    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
468	    // [impl->REQ-INSTALL-11] adapter install dir wins over PATH for a bare token.
469	    let program = match install_dir {
470	        Some(dir) => resolve_program_in_dir(program, dir),
471	        None => program.clone(),
472	    };
473	    let mut cmd = Command::new(&program);
474	    cmd.args(args)

/// lines that live *outside* the role set, so [`ManifestRuntime::run_bounded`]
/// (which looks a role up in the manifest) does not apply. `stdin` feeds a
/// `normalize_command` the raw transcript on its standard input; it is written
/// on a thread so a child that reads slowly never deadlocks the poll.
// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]
pub fn run_bounded_command(
    template: &str,
    keys: &BTreeMap<String, String>,
    timeout: Duration,
    stdin: Option<&[u8]>,
) -> Result<BoundedOutput, RuntimeError> {
    run_bounded_command_in(template, keys, timeout, stdin, None)
}

/// Resolve a command template's **program token** (first whitespace-split token)
/// against an adapter's install dir before falling back to `PATH`. A `.spt`
/// adapter ships its built binaries either to `adapters/_github/<safe>/` (via
/// `--release`/`--github` acquisition) or keeps them in the adapter's
/// `source_dir` under copy-mode — both are the adapter's `AdapterRecord`
/// `source_dir`. A bare program name (`claude-spt-digest`) would otherwise miss
/// the shipped binary and resolve only from `PATH`; resolving against the install
/// dir first makes the adapter self-contained.
///
/// Returns `<install_dir>/<program>` (absolute) when that file exists — on
/// Windows also trying the `.exe` suffix — else the original bare token
/// unchanged (the `PATH` fallback). Pure and side-effect-free apart from the
/// existence probe; never errors (an unresolvable token is simply passed through
/// for the OS to resolve / fail at spawn).
// [impl->REQ-INSTALL-11]
pub fn resolve_program_in_dir(program: &str, install_dir: &Path) -> String {
    let direct = install_dir.join(program);
    if direct.is_file() {
        return direct.to_string_lossy().into_owned();
    }
    #[cfg(windows)]
    {
        if Path::new(program).extension().is_none() {
            let exe = install_dir.join(format!("{program}.exe"));
            if exe.is_file() {
                return exe.to_string_lossy().into_owned();
            }
        }
    }
    program.to_string()
}

/// As [`run_bounded_command`], but when `install_dir` is `Some`, the template's
/// program token is first resolved against that adapter install dir
/// ([`resolve_program_in_dir`]) so a bare program name binds to the adapter's
/// shipped binary before `PATH` (REQ-INSTALL-11). `None` preserves the legacy
/// PATH-only resolution for callers without an install dir.
// [impl->REQ-INSTALL-11]
// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]
pub fn run_bounded_command_in(
    template: &str,
    keys: &BTreeMap<String, String>,
    timeout: Duration,
    stdin: Option<&[u8]>,
    install_dir: Option<&Path>,
) -> Result<BoundedOutput, RuntimeError> {
    let filled = fill_template(template, keys)?;
    let tokens = tokenize(&filled);
    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
    // [impl->REQ-INSTALL-11] adapter install dir wins over PATH for a bare token.
    let program = match install_dir {
        Some(dir) => resolve_program_in_dir(program, dir),
        None => program.clone(),
    };
    let mut cmd = Command::new(&program);
    cmd.args(args)


Line 421–424 is the key claim: for `--release`/`--github`, the binaries ship to `adapters/_github/<safe>/` and **that IS the `AdapterRecord.source_dir`**. If registration actually sets `source_dir` to the extracted `_github` dir, then livehost using `rec.source_dir` is correct — no divergence, and install-dir resolution works everywhere (proven by the passing E2E). Verifying registration sets `source_dir` for github/release mode:


crates\spt\src\cli.rs:481:    /// cloned under `adapters/_github/`). Manifest-first: an invalid manifest
crates\spt\src\cli.rs:2347:/// durable `adapters/_github/<safe>` home, returning the extracted root for
crates\spt\src\cli.rs:2360:    let github = adapters.join("_github");
crates\spt\src\cli.rs:5121:                    // adapters/_github/ (a durable home — pointer-mode records
crates\spt\src\cli.rs:5124:                    let dest = adapters.join("_github").join(safe);
crates\spt\src\cli.rs:5235:                    r.name, r.kind, r.mode, r.source_dir
crates\spt\src\cli.rs:5433:        //    fetched archive lands under adapters/_github/<safe> as raw bytes we
crates\spt\src\cli.rs:5468:        //    install in the durable _github home; pointer-mode re-reads it live).
crates\spt\src\cli.rs:5470:        let dest = adapters.join("_github").join(&safe);
crates\spt\src\cli.rs:5543:/// `_github` home (NOT yet extracted/registered), so they can be verified before
crates\spt\src\cli.rs:5554:    let github = adapters.join("_github");
crates\spt\src\cli.rs:5612:    // The record's `source_dir` is the adapter install dir — where a `.spt`
crates\spt\src\cli.rs:5616:        Ok((r, m)) => (std::path::PathBuf::from(r.source_dir), m),
crates\spt\src\api\mod.rs:475:///   name:profile`. The install dir is the record's precise `source_dir` (not a
crates\spt\src\api\mod.rs:495:            // `source_dir` is the precise install dir. An unregistered/dev
crates\spt\src\api\mod.rs:501:                    Some(PathBuf::from(&record.source_dir)),
crates\spt\src\api\mod.rs:614:    /// which is the record's `source_dir` (copy-mode provenance), the install dir
crates\spt\src\api\mod.rs:628:        PathBuf::from(record.source_dir)
crates\spt\src\api\mod.rs:633:    // install_dir to the record's precise source_dir.
crates\spt\src\api\mod.rs:639:        // Bare option: the parent's own advertisement, install_dir = source_dir.


5460	        } else {
5461	            eprintln!(
5462	                "ADAPTER_UPDATE_UNSIGNED:{}: no signing_key — trusting HTTPS + GitHub",
5463	                record.name
5464	            );
5465	        }
5466
5467	        // 5. Extract + re-register (the verified/trusted bytes become the new
5468	        //    install in the durable _github home; pointer-mode re-reads it live).
5469	        let safe = repo.replace(['/', '\\'], "-");
5470	        let dest = adapters.join("_github").join(&safe);
5471	        if dest.exists() {
5472	            let _ = std::fs::remove_dir_all(&dest);
5473	        }
5474	        if let Err(e) = std::fs::create_dir_all(&dest) {
5475	            let _ = std::fs::remove_file(&staged);
5476	            eprintln!("ADAPTER_UPDATE_FAIL:{}: create dest: {e}", record.name);
5477	            failed = true;
5478	            continue;
5479	        }
5480	        let extracted = extract_release_archive(&staged, &dest);
5481	        let _ = std::fs::remove_file(&staged);
5482	        if let Err(e) = extracted {
5483	            eprintln!("ADAPTER_UPDATE_FAIL:{}: extract: {e}", record.name);
5484	            failed = true;
5485	            continue;
5486	        }
5487	        match registry::register(adapters, &dest, now_ms()) {
5488	            Ok((r, _)) => eprintln!("ADAPTER_UPDATE_DONE:{} (re-registered)", r.name),
5489	            Err(e) => {
5490	                eprintln!("ADAPTER_UPDATE_FAIL:{}: re-register: {e}", record.name);
5491	                failed = true;
5492	            }
5493	        }
5494	    }
5495
5496	    i32::from(failed)
5497	}
5498
5499	/// Query the GitHub API for `repo`'s latest release `tag_name`, returning the
5500	/// version with any leading `v` stripped (e.g. `v0.2.0` → `0.2.0`). HTTPS +
5501	/// GitHub trust, same as the `--release` acquisition (REQ-UPD-9).
5502	// [impl->REQ-UPD-9]
5503	fn gh_latest_release_version(repo: &str) -> Result<String, String> {
5504	    let url = format!("https://api.github.com/repos/{repo}/releases/latest");

        } else {
            eprintln!(
                "ADAPTER_UPDATE_UNSIGNED:{}: no signing_key — trusting HTTPS + GitHub",
                record.name
            );
        }

        // 5. Extract + re-register (the verified/trusted bytes become the new
        //    install in the durable _github home; pointer-mode re-reads it live).
        let safe = repo.replace(['/', '\\'], "-");
        let dest = adapters.join("_github").join(&safe);
        if dest.exists() {
            let _ = std::fs::remove_dir_all(&dest);
        }
        if let Err(e) = std::fs::create_dir_all(&dest) {
            let _ = std::fs::remove_file(&staged);
            eprintln!("ADAPTER_UPDATE_FAIL:{}: create dest: {e}", record.name);
            failed = true;
            continue;
        }
        let extracted = extract_release_archive(&staged, &dest);
        let _ = std::fs::remove_file(&staged);
        if let Err(e) = extracted {
            eprintln!("ADAPTER_UPDATE_FAIL:{}: extract: {e}", record.name);
            failed = true;
            continue;
        }
        match registry::register(adapters, &dest, now_ms()) {
            Ok((r, _)) => eprintln!("ADAPTER_UPDATE_DONE:{} (re-registered)", r.name),
            Err(e) => {
                eprintln!("ADAPTER_UPDATE_FAIL:{}: re-register: {e}", record.name);
                failed = true;
            }
        }
    }

    i32::from(failed)
}

/// Query the GitHub API for `repo`'s latest release `tag_name`, returning the
/// version with any leading `v` stripped (e.g. `v0.2.0` → `0.2.0`). HTTPS +
/// GitHub trust, same as the `--release` acquisition (REQ-UPD-9).
// [impl->REQ-UPD-9]
fn gh_latest_release_version(repo: &str) -> Result<String, String> {
    let url = format!("https://api.github.com/repos/{repo}/releases/latest");


Confirmed: registration for `--github`/`--release` sets `source_dir` to the extracted `adapters/_github/<safe>/` root (cli.rs:5470,5487 → `register(adapters, &dest, …)`; `source_dir` = manifest's parent). So `source_dir` **is** the install dir for all modes, and livehost + the api seam (`resolve_ctx_manifest`) both use it — **no divergence**. The passing E2E + negative control prove install-dir resolution works.

This **reframes v0.8.1**, grounded in runtime:
- **Net-independence (REQ's primary mandate) ALREADY HOLDS** — proven by the real-daemon net-less E2E (Psyche hosted with net fully starved). No behavior fix; the E2E *locks it in*.
- **install-dir resolution is correct + consistent** (api + daemon agree on `source_dir`).
- **The real spt-core gap = the silent-failure masking the REQ title itself names** — `LIVEHOST_PSYCHE_FAIL` is `eprintln`-only on the brain's invisible stderr; the agent stays `online`/`alive` (stamped unconditionally) with no Psyche and no harness-visible diagnostic.

This contradicts the delegation's net-boot-race premise (doyle did flag H2, but runtime shows resolution is actually correct). That's a premise-shifting finding doyle should weigh — he has perri's real-env data I don't. Updating memory, then reporting the grounded reframe + proposed scope:


Edit

**LESSON IN FLIGHT:** an Explore subagent over-theorized a shaky root cause; ground in source (done for boot path) + RUNTIME repro (the E2E), not the agent's narrative. [[grep-req-tags-to-find-impl]] sibling lesson.

**W1 DONE — real-daemon E2E built + bug DID NOT reproduce as a net-race (RUNTIME-PROVEN reframe):** `crates/spt/tests/livehost_bootrace_e2e.rs` (`netless_online_live_agent_is_hosted_by_the_real_daemon_brain`) launches the REAL `spt daemon run` broker → real `spt daemon brain` CHILD, forces net-less (corrupt node.key → NODE_KEY_FAIL no-retry), real `api seed`+`listen` establish→online, REAL psyche binary (`psychebin`=copy of spt) by BARE token ONLY in install source_dir (exercises REQ-INSTALL-11), bounded 25s psyche-perch poll. **PASSES net-less** — `LIVEHOST_PSYCHE` fires with net fully starved. Negative control (binary removed) → `LIVEHOST_PSYCHE_FAIL: ... program not found` per 5s tick. So:
- **H1 net-gating REFUTED by RUNTIME** (not just source): spawn_live_host @brainproc:230 unconditional, before the heartbeat net-gate (consumer_gate gates ONLY dispatcher/peer-pump). Net-independence ALREADY HOLDS — the E2E locks it in; NO behavior fix needed there.
- **install-dir resolution CORRECT + CONSISTENT:** github/release registration sets `source_dir = adapters/_github/<safe>/` (cli.rs:5470,5487; runtime.rs:421-424), copy-mode source_dir = manifest dir; livehost (rec.source_dir) AND api seam (resolve_ctx_manifest, mod.rs:501) use the SAME source_dir. The W1 "divergence" candidate REFUTED. resolve_program_in_dir (runtime.rs:434) works (E2E proves).
- **REAL spt-core gap = SILENT-FAILURE MASKING (the REQ title names it):** spawn_psyche Err → `LIVEHOST_PSYCHE_FAIL` is eprintln-ONLY on the brain's INVISIBLE stderr (brainproc.rs:927-948 brain inherits broker stderr → foreground `spt daemon run`, unseen by a harness watching only `api listen` stdout). Agent stays state=live_agent+alive=true+status=online (cmd_listen stamps unconditionally, startup.rs:284) with NO Psyche, NO harness-visible error. = perri's exact diagnostic.

**PROPOSED v0.8.1 SCOPE (pending doyle confirm — premise shifted):** (1) KEEP the real-daemon E2E as the non-negotiable gate artifact; re-tag its evidence to `[int->REQ-HAZARD-LIVEHOST-BOOT-RACE]` (W1 tagged it REQ-DAEMON-1). (2) SURFACE the silent host-failure — write a disk-observable signal on the live perch when spawn_psyche fails (so "online but un-hosted" is diagnosable by the harness), the real behavioral fix for the masking. (3) net-independence = E2E regression-lock only (already holds). perri's actual binary: present-but-unresolved (would need an spt-core resolution fix — but E2E says resolution works) vs absent (adapter packaging, not spt-core, but now surfaced) — doyle has perri's env to disambiguate.

**LESSON:** the Explore root-cause agent (afe7959) over-theorized a shaky "registry race" — REFUTED. The reproduce-agent (af5fc4ee) with a NEGATIVE CONTROL nailed it. Ground in RUNTIME repro, not agent narrative. [[grep-req-tags-to-find-impl]] sibling.


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

**W1 DONE — real-daemon E2E built + bug DID NOT reproduce as a net-race (RUNTIME-PROVEN reframe):** `crates/spt/tests/livehost_bootrace_e2e.rs` (`netless_online_live_agent_is_hosted_by_the_real_daemon_brain`) launches the REAL `spt daemon run` broker → real `spt daemon brain` CHILD, forces net-less (corrupt node.key → NODE_KEY_FAIL no-retry), real `api seed`+`listen` establish→online, REAL psyche binary (`psychebin`=copy of spt) by BARE token ONLY in install source_dir (exercises REQ-INSTALL-11), bounded 25s psyche-perch poll. **PASSES net-less** — `LIVEHOST_PSYCHE` fires with net fully starved. Negative control (binary removed) → `LIVEHOST_PSYCHE_FAIL: ... program not found` per 5s tick. So:
- **H1 net-gating REFUTED by RUNTIME** (not just source): spawn_live_host @brainproc:230 unconditional, before the heartbeat net-gate (consumer_gate gates ONLY dispatcher/peer-pump). Net-independence ALREADY HOLDS — the E2E locks it in; NO behavior fix needed there.
- **install-dir resolution CORRECT + CONSISTENT:** github/release registration sets `source_dir = adapters/_github/<safe>/` (cli.rs:5470,5487; runtime.rs:421-424), copy-mode source_dir = manifest dir; livehost (rec.source_dir) AND api seam (resolve_ctx_manifest, mod.rs:501) use the SAME source_dir. The W1 "divergence" candidate REFUTED. resolve_program_in_dir (runtime.rs:434) works (E2E proves).
- **REAL spt-core gap = SILENT-FAILURE MASKING (the REQ title names it):** spawn_psyche Err → `LIVEHOST_PSYCHE_FAIL` is eprintln-ONLY on the brain's INVISIBLE stderr (brainproc.rs:927-948 brain inherits broker stderr → foreground `spt daemon run`, unseen by a harness watching only `api listen` stdout). Agent stays state=live_agent+alive=true+status=online (cmd_listen stamps unconditionally, startup.rs:284) with NO Psyche, NO harness-visible error. = perri's exact diagnostic.

**PROPOSED v0.8.1 SCOPE (pending doyle confirm — premise shifted):** (1) KEEP the real-daemon E2E as the non-negotiable gate artifact; re-tag its evidence to `[int->REQ-HAZARD-LIVEHOST-BOOT-RACE]` (W1 tagged it REQ-DAEMON-1). (2) SURFACE the silent host-failure — write a disk-observable signal on the live perch when spawn_psyche fails (so "online but un-hosted" is diagnosable by the harness), the real behavioral fix for the masking. (3) net-independence = E2E regression-lock only (already holds). perri's actual binary: present-but-unresolved (would need an spt-core resolution fix — but E2E says resolution works) vs absent (adapter packaging, not spt-core, but now surfaced) — doyle has perri's env to disambiguate.

**LESSON:** the Explore root-cause agent (afe7959) over-theorized a shaky "registry race" — REFUTED. The reproduce-agent (af5fc4ee) with a NEGATIVE CONTROL nailed it. Ground in RUNTIME repro, not agent narrative. [[grep-req-tags-to-find-impl]] sibling.

---
name: v081-livehost-bootrace
description: "v0.8.1 task — fix REQ-HAZARD-LIVEHOST-BOOT-RACE: net-less node, live agent goes online but daemon never hosts Psyche (no LIVEHOST_PSYCHE). doyle delegation, I own waves+execution. Verified root-cause-so-far + wave plan + the non-negotiable real-daemon E2E gate."
metadata:
  node_type: memory
  type: project
  originSessionId: 8350d43e-fe2d-4e24-9999-eecbcc6d3a50
---

**v0.8.1 livehost boot-race** — doyle delegation (operator-directed), I own W/T planning + execution, doyle gates + drives release (counter 17). Branch `v0.8.1-livehost-bootrace` off main (v0.8.0), base @2aab895 mints `REQ-HAZARD-LIVEHOST-BOOT-RACE` (required_stages=[]; I activate [impl,unit,int] per wave, same-commit tags). PR → ping doyle at PR-ready (self-drive until then). See [[v080-merge-reconciliation]] (the v0.8.0 livehost reapply this builds on).

**DEFECT (perri v0.8.0 dogfood, real bug):** harness-hosted live agent (`spt api seed` + `spt api listen <id>`) goes status=online but NEVER gets a daemon-hosted Psyche on a net-less/unpaired/peer-pump-STALLED node. Host-predicate fully passes (state=live_agent, status=online, adapter resolvable, psyche_init present) yet `reconcile_once` never emits `LIVEHOST_PSYCHE`. "peer pump: STALLED" reappears after every restart.

**VERIFIED from source (brainproc.rs:159-255):** run_brain :161 `connect_retry(&broker_socket)?` connects to the LOCAL BROKER (not net); broker binds its IPC listener INDEPENDENT of net (broker.rs:575 bind_in_with_net). So net-less the broker-connect succeeds → :230 `spawn_live_host` IS reached (unconditional, own thread, before the heartbeat loop). The heartbeat loop (:233) net-gates ONLY the consumers (dispatcher/peer pump) via consumer_gate, NOT livehost. ⇒ **H1 (brain starved before :230 by net) REFUTED by source** — the live reconcile thread runs net-less. (Caveat NOT yet verified: whether the BROKER itself spawns the brain child net-less, or the broker's own boot stalls before binding — H3/broker-side, open.)

**Explore report (afe7959) — TREAT WITH CAUTION, its mechanism is shaky:** concluded "registry load-order race → install_dir=None → spawn_psyche fails." INTERNALLY INCONSISTENT: `resolve_option_in(adapter)` and the install_dir `find` use the SAME `registered` slice + same parent — can't succeed-then-miss. And a first-tick registry miss self-heals next tick (5s) ≠ the PERSISTENT defect. So the specific theory is likely WRONG. doyle warned "do NOT presume (perri refuted doyle's first guess)."

**TRUE failure = runtime-observable only.** Real candidates: (a) `spawn_psyche` persistently fails → `LIVEHOST_PSYCHE_FAIL` every tick — most likely REQ-INSTALL-11 psyche-binary-not-found (perri's blocked proof, copy-mode binary absent from install_dir+PATH); (b) H3 broker doesn't spawn brain child net-less; (c) reconcile predicate fails on a field. doyle's brief: "check LIVEHOST_PSYCHE_FAIL."

**WAVE PLAN:**
- **W1 — reproduce via the non-negotiable gate artifact:** a REAL detached-daemon E2E (spawn the real brain CHILD via `spt daemon`, NOT in-process; existing live_firsthost_e2e uses in-process daemon + hand-writes perch, so the production broker→brain-child→run_brain→spawn_live_host→reconcile path was NEVER E2E-tested). `api seed` + `api listen` a live agent on a net-less/unpaired node → assert `LIVEHOST_PSYCHE`. Watch it FAIL → read the ACTUAL failure (LIVEHOST_PSYCHE_FAIL text / behavior). Ground the fix in THIS, not the report. (Look at crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs for real-brain-spawn infra.)
- **W2 — fix** per observed failure: live-host reconcile runs net-INDEPENDENTLY + survives the boot-race, self-healing like REQ-DAEMON-9 (dispatcher/peer-pump boot-race self-heal in broker.rs attach_net). Likely also fix REQ-INSTALL-11 psyche binary resolution if that's the observed failure.
- **W3 — green + activate + gate.**

**WHY IT MATTERS (doyle):** unblocks perri's last 2 parity proofs — REQ-INSTALL-11 install-dir binary resolution (psyche must actually SPAWN to exercise it) + F-006 interim PATH-copy.

**GATE (pre-flight before pinging doyle):** `clippy --workspace --all-targets -D warnings` (EXACT CI invocation, NOT -p) · `traceable-reqs check` EXIT=0 · docs-drift `xtask gen` if any CLI touched · the real-daemon E2E GREEN. Then doyle code-reads + verifies the E2E spawns the brain CHILD + gates → deployah ships v0.8.1 (counter 17) → doyle pings perri.

**LESSON IN FLIGHT:** an Explore subagent over-theorized a shaky root cause; ground in source (done for boot path) + RUNTIME repro (the E2E), not the agent's narrative. [[grep-req-tags-to-find-impl]] sibling lesson.

-**LESSON IN FLIGHT:** an Explore subagent over-theorized a shaky root cause; ground in source (done for boot path) + RUNTIME repro (the E2E), not the agent's narrative. [[grep-req-tags-to-find-impl]] sibling lesson.

+**W1 DONE — real-daemon E2E built + bug DID NOT reproduce as a net-race (RUNTIME-PROVEN reframe):** `crates/spt/tests/livehost_bootrace_e2e.rs` (`netless_online_live_agent_is_hosted_by_the_real_daemon_brain`) launches the REAL `spt daemon run` broker → real `spt daemon brain` CHILD, forces net-less (corrupt node.key → NODE_KEY_FAIL no-retry), real `api seed`+`listen` establish→online, REAL psyche binary (`psychebin`=copy of spt) by BARE token ONLY in install source_dir (exercises REQ-INSTALL-11), bounded 25s psyche-perch poll. **PASSES net-less** — `LIVEHOST_PSYCHE` fires with net fully starved. Negative control (binary removed) → `LIVEHOST_PSYCHE_FAIL: ... program not found` per 5s tick. So:

+- **H1 net-gating REFUTED by RUNTIME** (not just source): spawn_live_host @brainproc:230 unconditional, before the heartbeat net-gate (consumer_gate gates ONLY dispatcher/peer-pump). Net-independence ALREADY HOLDS — the E2E locks it in; NO behavior fix needed there.

+- **install-dir resolution CORRECT + CONSISTENT:** github/release registration sets `source_dir = adapters/_github/<safe>/` (cli.rs:5470,5487; runtime.rs:421-424), copy-mode source_dir = manifest dir; livehost (rec.source_dir) AND api seam (resolve_ctx_manifest, mod.rs:501) use the SAME source_dir. The W1 "divergence" candidate REFUTED. resolve_program_in_dir (runtime.rs:434) works (E2E proves).

+- **REAL spt-core gap = SILENT-FAILURE MASKING (the REQ title names it):** spawn_psyche Err → `LIVEHOST_PSYCHE_FAIL` is eprintln-ONLY on the brain's INVISIBLE stderr (brainproc.rs:927-948 brain inherits broker stderr → foreground `spt daemon run`, unseen by a harness watching only `api listen` stdout). Agent stays state=live_agent+alive=true+status=online (cmd_listen stamps unconditionally, startup.rs:284) with NO Psyche, NO harness-visible error. = perri's exact diagnostic.

+**PROPOSED v0.8.1 SCOPE (pending doyle confirm — premise shifted):** (1) KEEP the real-daemon E2E as the non-negotiable gate artifact; re-tag its evidence to `[int->REQ-HAZARD-LIVEHOST-BOOT-RACE]` (W1 tagged it REQ-DAEMON-1). (2) SURFACE the silent host-failure — write a disk-observable signal on the live perch when spawn_psyche fails (so "online but un-hosted" is diagnosable by the harness), the real behavioral fix for the masking. (3) net-independence = E2E regression-lock only (already holds). perri's actual binary: present-but-unresolved (would need an spt-core resolution fix — but E2E says resolution works) vs absent (adapter packaging, not spt-core, but now surfaced) — doyle has perri's env to disambiguate.

+**LESSON:** the Explore root-cause agent (afe7959) over-theorized a shaky "registry race" — REFUTED. The reproduce-agent (af5fc4ee) with a NEGATIVE CONTROL nailed it. Ground in RUNTIME repro, not agent narrative. [[grep-req-tags-to-find-impl]] sibling.


Bash

cat <<'EOF' | "$OWL" send doyle
todlando — v0.8.1 root-cause GROUNDED IN RUNTIME, and it REFRAMES the hazard. Built the non-negotiable gate artifact FIRST and used it to reproduce — bug did NOT reproduce as a net-race.

REAL-DAEMON E2E (built): crates/spt/tests/livehost_bootrace_e2e.rs — launches the REAL `spt daemon run` broker → real `spt daemon brain` CHILD (process, not in-process), net-less forced (corrupt node.key → NODE_KEY_FAIL, no retry), REAL `api seed`+persistent `api listen` establish→online, REAL psyche binary by a BARE token present ONLY in the adapter install source_dir (so REQ-INSTALL-11 install-dir resolution is the ONLY way it resolves), bounded 25s. It PASSES net-less: LIVEHOST_PSYCHE fires with net fully starved. Negative control (psyche binary removed) → LIVEHOST_PSYCHE_FAIL "program not found" per tick.

FINDINGS (runtime + source):
1. H1 net-gating REFUTED BY RUNTIME: spawn_live_host @brainproc:230 is unconditional, BEFORE the heartbeat net-gate (consumer_gate gates only dispatcher/peer-pump). Net-independence ALREADY HOLDS on this tree — no behavior fix needed; the E2E locks it in.
2. install-dir resolution CORRECT + CONSISTENT: github/release registration sets source_dir = adapters/_github/<safe>/ (cli.rs:5470,5487); livehost (rec.source_dir) AND the api seam (resolve_ctx_manifest mod.rs:501) use the SAME source_dir. No divergence. resolve_program_in_dir works (E2E proves). So REQ-INSTALL-11 resolution is sound when the binary is actually in the install dir.
3. THE REAL spt-core GAP = the SILENT-FAILURE MASKING your REQ title names: spawn_psyche Err → LIVEHOST_PSYCHE_FAIL is eprintln-ONLY on the brain's INVISIBLE stderr (brain inherits the broker/foreground `spt daemon run` stderr; a harness watching only `api listen` stdout never sees it). Agent stays live_agent+alive+online (stamped unconditionally, startup.rs:284) with NO Psyche, NO harness-visible error = perri's exact symptom.

SO the net-boot-race premise doesn't hold here (you flagged H2 — it's closer, but resolution itself is correct). perri's real failure is most likely her adapter's psyche binary NOT being in the install dir (packaging) OR not shipped — which spt-core currently SWALLOWS silently.

PROPOSED v0.8.1 SCOPE:
A. KEEP the real-daemon E2E as the gate artifact (re-tag [int->REQ-HAZARD-LIVEHOST-BOOT-RACE]).
B. SURFACE the silent host-failure: write a disk-observable signal on the live perch when spawn_psyche fails, so "online but un-hosted" is diagnosable by the harness (the real fix for the masking).
C. net-independence = regression-lock via the E2E (already holds).

DECISION NEEDED (you have perri's env, I don't): is perri's psyche binary present-but-misresolved (→ real spt-core resolution bug to hunt) or absent/mispackaged (→ B surfaces it, adapter-side packaging fix)? Confirm scope A+B+C, or redirect. I can start B immediately on your nod — it's wanted under any reading.
EOF
echo "EXIT=$?"

Report grounded reframe + proposed scope to doyle


➜ SENT:doyle
EXIT=0


Reported to doyle (`SENT:doyle`).

**v0.8.1 root cause — grounded in runtime, reframes the hazard:**
- Built the non-negotiable real-daemon E2E (`livehost_bootrace_e2e.rs`: real `spt daemon run`→brain **child**, net-less, real psyche binary by bare token in install dir) and used it to reproduce. **It passes net-less** — `LIVEHOST_PSYCHE` fires with net fully starved.
- **H1 net-gating refuted by runtime**; net-independence already holds (`spawn_live_host` @:230 unconditional, before the net-gate). **install-dir resolution correct + consistent** (livehost + api seam both use `source_dir`; github/release `source_dir` = `_github` root). Negative control (binary removed) → `LIVEHOST_PSYCHE_FAIL`.
- **Real gap = the silent-failure masking the REQ title names:** `LIVEHOST_PSYCHE_FAIL` is `eprintln`-only on the brain's invisible stderr → agent stays online/alive with no Psyche, no harness-visible error = perri's symptom.

Proposed scope: **(A)** keep the E2E as gate artifact, **(B)** surface the silent host-failure on the live perch (disk-observable), **(C)** net-independence = regression-lock via the E2E. Asked doyle to disambiguate (he has perri's env): is perri's psyche binary present-but-misresolved (→ real resolution bug to hunt) or absent/mispackaged (→ B surfaces it). Offered to start B on his nod.

Holding for doyle's scope steer — genuine premise fork, and he has the perri-env data. Findings durable in memory.


<task-notification>
<task-id>be1did0ug</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="cdbfb8c4" type="msg" from="doyle">GATER RULING — reframe ENDORSED. Excellent runtime root-cause (built the gate artifact first, reproduced, refuted the net-race by RUNTIME not argument — exactly right). The REQ title's masking clause already names this; net-race was the wrong half. v0.8.1 scope A/B/C APPROVED with 4 refinements:

1. The host-f</EVENT-PART>
<EVENT-PART seq="2/7" id="cdbfb8c4">ailure signal MUST be HARNESS-REACHABLE, not just a log. perri reads PERCH STATE, never the brain's stderr. Land it as an info.json field on the live perch (e.g. psyche_host_error / last_host_fail + reason) surfaced by `spt endpoint list`/status (or an api state verb). That's the actual fix for perri's blindness — a disk eprintln she can't s</EVENT-PART>
<EVENT-PART seq="3/7" id="cdbfb8c4">ee is no better.

2. Do NOT gate/de-stamp status=online on host-failure. The agent IS online (reachable for messages); the missing piece is only the Psyche lifecycle. Brain-restart rehydrate (CONTEXT:194) legitimately has online-with-no-Psyche-yet windows. So: a SEPARATE psyche-host-health field, online stays authoritative.

3. Ide</EVENT-PART>
<EVENT-PART seq="4/7" id="cdbfb8c4">mpotent/rate-limited: reconcile retries every 5s (LIVE_RECONCILE_INTERVAL_MS) → a permanently-missing binary would spam. Make it a CURRENT-STATE stamp (overwrite last_host_error + ts + attempt count), CLEARED on successful host — not an append log.

4. REQ TITLE delta: since net-independence already holds (E2E LOCKS it, no behavior c</EVENT-PART>
<EVENT-PART seq="5/7" id="cdbfb8c4">hange), the REQ's PRIMARY deliverable shifts to &quot;surface the silent psyche-host failure on the perch (harness-diagnosable)&quot;, with net-independence as a locked-in invariant. I own the registry — propose your title wording and I'll commit it, or I'll redraft. Activate [impl,unit,int]; int = the E2E (positive net-less host + negative </EVENT-PART>
<EVENT-PART seq="6/7" id="cdbfb8c4">missing-binary→signal-written-on-perch).

BOUNDARY: spt-core SURFACES the failure; it does NOT fix perri's packaging (her psyche binary missing from the install dir is her adapter's job). Once surfaced, perri diagnoses + fixes her side.

DOCS: new perch field is observable behavior → CONTEXT note + docs-drift gen if any CLI ren</EVENT-PART>
<EVENT-PART seq="7/7" id="cdbfb8c4">der changes + tag doc evidence.

RESEND part 9/9 (truncated again at 8 — the owl part-cap; the tail of C + anything after). Then proceed. Ping doyle at PR-ready for gate.</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>


doyle **endorsed the reframe**; scope A/B/C approved with 4 crisp refinements:
1. Signal must be **harness-reachable** — an `info.json` field on the live perch (perri reads perch state, never brain stderr), surfaced by `spt endpoint list`/status.
2. Do **NOT** de-stamp `status=online` — agent IS reachable; only the Psyche is missing (brain-restart rehydrate has legit online-without-Psyche windows). Separate psyche-host-health field; online stays authoritative.
3. **Current-state stamp** (overwrite reason+ts+attempt-count, CLEARED on successful host) — not an append log (5s reconcile would spam).
4. REQ primary deliverable shifts to "surface the silent psyche-host failure on the perch"; net-independence = locked-in invariant. Activate [impl,unit,int]; int = E2E positive net-less host + negative missing-binary→signal-on-perch. Boundary: spt-core surfaces; perri owns her packaging.

Part 9/9 truncated again — requesting resend. Sending doyle the proposed REQ-title wording + part-9 request, and spawning the build agent with the precise spec (the E2E already exists on disk for it to extend):


cat <<'EOF' | "$OWL" send doyle
todlando — scope A/B/C + 4 refinements ACK, all crisp, building now. Two things:

PROPOSED REQ-HAZARD-LIVEHOST-BOOT-RACE TITLE (you commit/redraft — you own the registry):
"The brain's daemon-hosted Psyche lifecycle surfaces a host-FAILURE on the live perch (harness-diagnosable) and runs net-INDEPENDENTLY. When reconcile_once→host_one→spawn_psyche fails for a state=live_agent+status=online endpoint (e.g. the adapter's psyche binary absent from its install dir, REQ-INSTALL-11), the failure MUST be written to the perch info.json as a CURRENT-STATE field (reason + ts + attempt count; overwritten each 5s retry, CLEARED on successful host) and surfaced by `spt endpoint list`/status — never left as an eprintln on the brain's invisible stderr where a harness reading only perch state is blind. status=online stays authoritative (agent reachable; only the Psyche is missing — brain-restart rehydrate legitimately has online-without-Psyche windows), so this is a SEPARATE psyche-host-health field, never a status de-stamp. Net-independence is a locked-in invariant: spawn_live_host (brainproc.rs:230) reaches the reconcile and hosts the Psyche on a net-less/unpaired/peer-pump-STALLED node, proven by a REAL detached-daemon E2E (real broker→brain-child, real api seed+listen, real install-dir psyche binary). spt-core SURFACES the failure; the adapter owns fixing its packaging."

PART 9/9 truncated AGAIN at part 8 (owl part-cap) — resend the tail of refinement C + anything after. Proceeding on what I have (A/B/C + 4 refinements are complete enough to build).

Will ping you at PR-ready with the full gate (clippy workspace, traceable EXIT0, the E2E positive+negative, docs-drift).
EOF
echo "EXIT=$?"

Send REQ title proposal + part-9 request to doyle


Now the build agent — complete self-contained spec (doyle's 4 refinements), targeted runs only, commit on the branch for me to verify + gate: