1	//! v0.8.1 boot-race REPRODUCER + v0.8.2 RESIDENT-path proof (REQ-DAEMON-1 /
2	//! REQ-HAZARD-LIVEHOST-NONRESIDENT) — a harness-hosted live agent goes
3	//! `status=online` and the REAL daemon brain hosts its Psyche on a net-less /
4	//! unpaired node, the host CONFIRMS RESIDENCY (the psyche stays alive past the
5	//! grace window), and the parent perch shows clean (no `psyche_host_error`).
6	//!
7	//! This is the full-stack production-path E2E that the existing analogs stop short
8	//! of (doyle's mandate):
9	//!
10	//!   - `daemon_lifecycle_real_brain.rs` runs `run_brain` on a THREAD, uses a NOOP
11	//!     `cmd /C rem` psyche (a shell builtin that always resolves — it never
12	//!     exercises install-dir binary resolution), and HAND-WRITES the online perch.
13	//!   - This test launches the REAL `spt daemon run` broker PROCESS (which spawns a
14	//!     real supervised `spt daemon brain` CHILD), drives the REAL `api seed` +
15	//!     `api listen` establish→online path, and uses a REAL psyche binary referenced
16	//!     by a BARE program token that lives ONLY in the adapter install dir — so the
17	//!     daemon-host site MUST resolve it via REQ-INSTALL-11 install-dir resolution,
18	//!     not PATH.
19	//!
20	//! v0.8.2 (F-010) RESIDENCY note — the psyche fixture is now LONG-LIVED. Post-F-010
21	//! a fast-exiting psyche is a FAILED host by definition (residency confirm fires a
22	//! `psyche_host_error` once past the ~5s grace), so the old `psychebin --version`
23	//! fixture would have made THIS positive test a hollow pass that only held by racing
24	//! the brain before the grace — flake-prone on the slow shared kitsubito runner, and
25	//! it left the RESIDENT arm with ZERO E2E coverage. The fixture is therefore a copy
26	//! of the real `spt` binary invoked by the BARE token as `psychebin ready
27	//! <throwaway-id>` — `spt ready` (non-`--once`) is a persistent message listener that
28	//! BLOCKS in an infinite `recv()` loop needing only `SPT_HOME` (which the test sets),
29	//! so the spawned psyche STAYS ALIVE until we reap it. (Chosen over a bespoke sleeper
30	//! binary: it reuses the same real `spt` copy + bare-token install-dir resolution the
31	//! negative tests use — full REQ-INSTALL-11 fidelity, zero new fixture machinery, and
32	//! cross-platform on both CI runners.) The test then WAITS past the grace + one
33	//! reconcile tick and asserts the residency CONFIRMED: the nested `<id>-psyche` pid is
34	//! STILL ALIVE and the parent carries NO `psyche_host_error`.
35	//!
36	//! The disk-observable proxy for "the brain hosted the Psyche" is the nested
37	//! `<id>-psyche` perch's `info.json` appearing (mirrors `daemon_lifecycle_real_brain.rs`).
38	//! On a buggy tree this never appears within the bounded deadline.
39	//!
40	//! Bounded by design: every captured `spt` call has a deadline and every host/residency
41	//! wait is a bounded polling loop (<=30s), so a reproduced failure is a clean assert,
42	//! never a hang. All spawned processes (broker + supervised brain + listen + the
43	//! resident psyche) are reaped — scoped to OUR pids only, never a machine-wide reap.
44	// [int->REQ-DAEMON-1]
45	// [int->REQ-HAZARD-LIVEHOST-NONRESIDENT]
46
47	use std::path::{Path, PathBuf};
48	use std::process::{Child, Command, Output, Stdio};
49	use std::time::{Duration, Instant};
50
51	mod common;
52	use common::CommandNoWindowExt;
53
54	use spt_store::perch::{self, ParentHint};
55
56	/// Best-effort cross-platform kill of a pid tree (mirrors the other E2E helpers).
57	/// Scoped to a SINGLE pid we spawned — never a machine-wide reap (a live broker
58	/// shares this box).
59	fn kill_pid(pid: u32) {
60	    #[cfg(windows)]
61	    let _ = Command::new("taskkill")
62	        .no_window()
63	        .args(["/PID", &pid.to_string(), "/F", "/T"])
64	        .output();
65	    #[cfg(unix)]
66	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
67	}
68
69	/// `Command::output()` with a deadline, run off-thread — bounds a hang into a
70	/// clean failure (the `live_firsthost_e2e` pattern).
71	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
72	    let (tx, rx) = std::sync::mpsc::channel();
73	    std::thread::spawn(move || {
74	        let _ = tx.send(cmd.output());
75	    });
76	    rx.recv_timeout(deadline)
77	        .expect("captured spt call must complete within its deadline")
78	        .expect("run spt")
79	}
80
81	/// Read the brain `pid` out of a `brain.ready` body (self-describing JSON since
82	/// D6-1b). The come-up proof: the broker spawned a brain child.
83	fn ready_pid(body: &str) -> Option<u32> {
84	    serde_json::from_str::<serde_json::Value>(body)
85	        .ok()?
86	        .get("pid")?
87	        .as_u64()
88	        .map(|p| p as u32)
89	}
90
91	/// Poll `brain.ready` for a pid, up to `budget`. `None` if it never appears.
92	fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
93	    let deadline = Instant::now() + budget;
94	    while Instant::now() < deadline {
95	        if let Ok(s) = std::fs::read_to_string(path) {
96	            if let Some(pid) = ready_pid(&s) {
97	                return Some(pid);
98	            }
99	        }
100	        std::thread::sleep(Duration::from_millis(50));
101	    }
102	    None
103	}
104
105	#[test]
106	fn netless_online_live_agent_is_hosted_by_the_real_daemon_brain() {
107	    // ── (1) Isolated SPT_HOME, NET-LESS / UNPAIRED: we create no subnet and run no
108	    //    pairing. The whole defect window is "online live agent on a node with no
109	    //    net". One test in this binary owns the process-global SPT_HOME; children
110	    //    are passed it explicitly via .env() anyway. ──
111	    let home = tempfile::tempdir().unwrap();
112	    std::env::set_var("SPT_HOME", home.path());
113
114	    // Force the truest NET-LESS / UNPAIRED condition: a CORRUPT node.key makes
115	    // `nodeid::load_or_create()` error, so `Daemon::run` computes `node_hex = None`
116	    // → it NEVER calls `try_start_net` and arms NO net-retry (daemon.rs:153-161,
117	    // "NODE_KEY_FAIL: identity unavailable — broker runs net-less, no retry"). The
118	    // brain's `net-status` therefore never reports `enabled`, so BRAIN_NET_CONSUMERS_UP
119	    // never fires — the exact net-starved node perri hit. (A corrupt key is NEVER
120	    // auto-regenerated by design — nodeid.rs module docs.)
121	    let identity_dir = home.path().join("identity");
122	    std::fs::create_dir_all(&identity_dir).unwrap();
123	    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();
124
125	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
126	    let exe_suffix = spt_bin
127	        .extension()
128	        .map(|e| format!(".{}", e.to_string_lossy()))
129	        .unwrap_or_default();
130
131	    // ── (2) A live-capable adapter in COPY mode whose [session.psyche_init].command
132	    //    is a BARE program token (`psychebin`) naming a REAL psyche binary that
133	    //    ships in the adapter's install dir (= the registered `source_dir`). The
134	    //    fixture is a copy of the real `spt` binary invoked as `psychebin ready
135	    //    <throwaway-id>` — a LONG-LIVED psyche (`spt ready`, non-`--once`, BLOCKS in
136	    //    an infinite recv() loop until killed; needs only SPT_HOME). Post-F-010 a
137	    //    fast-exiting psyche is a FAILED host, so the resident-positive proof REQUIRES
138	    //    a psyche that stays alive past the residency grace. The bare token is NOT on
139	    //    PATH, so spawn_psyche can ONLY resolve it via REQ-INSTALL-11 install-dir
140	    //    resolution. ──
141	    let src = perch::spt_home().join("srcs").join("mocklive");
142	    std::fs::create_dir_all(&src).unwrap();
143
144	    // The real psyche binary fixture (a copy of `spt`), by a BARE name, IN the
145	    // install dir only — invoked below as the long-lived `psychebin ready ...`.
146	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
147	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the real psyche binary fixture");
148	    assert!(
149	        psyche_bin.is_file(),
150	        "the psyche binary fixture must exist in the install dir"
151	    );
152
153	    // The drops dir (commune/signoff watch) — irrelevant to the host proof but the
154	    // manifest needs a [session] block.
155	    let drops = home.path().join("drops");
156	    std::fs::create_dir_all(&drops).unwrap();
157	    let dir = drops.to_string_lossy().replace('\\', "/");
158
159	    // Command is the BARE token `psychebin` + a BLOCKING subcommand. NOT an
160	    // absolute path, NOT a shell builtin: it resolves only if install-dir
161	    // resolution fires. `ready <throwaway-id>` makes the spawned psyche a
162	    // persistent message listener that blocks until reaped — a LONG-LIVED resident
163	    // (multi-word value is single-token here, so it survives the fill order). The
164	    // throwaway id is namespaced so it never collides with the live agent's id.
165	    let manifest_toml = format!(
166	        "[adapter]\nname=\"mocklive\"\nkind=\"harness\"\nversion=\"1\"\n\
167	         min_spt_core_version=\"0\"\n\n\
168	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
169	         [session.psyche_init]\ncommand='psychebin ready bootrace-resident-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
170	    );
171	    let manifest_path = src.join("manifest.toml");
172	    std::fs::write(&manifest_path, &manifest_toml).unwrap();
173	    // Register in COPY mode (no [update] avenue ⇒ copy). The record's `source_dir`
174	    // is `src` — the install dir the brain resolves `psychebin` against.
175	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
176
177	    // Sanity: install-dir resolution of the bare token DOES find the fixture (so a
178	    // failure later is the WIRING not the fixture).
179	    let resolved =
180	        spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mocklive").map(|(rec, _)| {
181	            spt_runtime::runtime::resolve_program_in_dir("psychebin", Path::new(&rec.source_dir))
182	        });
183	    assert!(
184	        resolved
185	            .as_ref()
186	            .map(|p| Path::new(p).is_file())
187	            .unwrap_or(false),
188	        "install-dir resolution must find the bare psychebin in source_dir (got {resolved:?})"
189	    );
190
191	    // ── (3) Launch the REAL daemon broker PROCESS. `spt daemon run` is the broker;
192	    //    it spawns a supervised `spt daemon brain` CHILD. Redirect the broker's
193	    //    stderr to a log file in the tempdir — the brain child inherits the broker's
194	    //    stderr (spawn_brain_child sets no stdio), so BRAIN_UP / LIVEHOST_PSYCHE /
195	    //    LIVEHOST_PSYCHE_FAIL (all eprintln! to the brain's stderr) land here. ──
196	    let brain_log = home.path().join("brain.stderr.log");
197	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
198	    let mut broker: Child = Command::new(&spt_bin)
199	        .no_window()
200	        .args(["daemon", "run"])
201	        .env("SPT_HOME", home.path())
202	        .stdout(Stdio::null())
203	        .stderr(Stdio::from(brain_log_file))
204	        .spawn()
205	        .expect("spawn spt daemon run (broker process)");
206
207	    // Come-up: the broker spawned a brain child that signalled ready. This is the
208	    // precondition — if the brain never comes up, the host claim is moot.
209	    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
210	    {
211	        Some(p) => p,
212	        None => {
213	            let _ = broker.kill();
214	            let _ = broker.wait();
215	            panic!(
216	                "PRECONDITION FAILED: the real broker never spawned a ready brain child \
217	                 (brain.ready absent). Brain stderr:\n{}",
218	                std::fs::read_to_string(&brain_log).unwrap_or_default()
219	            );
220	        }
221	    };
222
223	    // ── (4) The REAL harness establish→online path: `api seed` then `api listen`.
224	    //    Seed keyed by THIS process's pid (alive — passes the recycle guard). The
225	    //    listen is the PERSISTENT (non --once) form: it marks the perch online and
226	    //    then BLOCKS in the relay loop until killed (the real harness leaves it
227	    //    running). Run it as a captured child whose stderr we keep, so we can see
228	    //    its BOUND/READY markers and reap it. ──
229	    let id = "bootrace-agent";
230	    let pid = std::process::id().to_string();
231	    let mp = manifest_path.to_string_lossy().to_string();
232
233	    let seed = {
234	        let mut cmd = Command::new(&spt_bin);
235	        cmd.no_window()
236	            .args([
237	                "api",
238	                "--adapter",
239	                "mocklive",
240	                "seed",
241	                "--pid",
242	                &pid,
243	                "--session-id",
244	                "sid-bootrace",
245	            ])
246	            .env("SPT_HOME", home.path());
247	        output_bounded(cmd, Duration::from_secs(30))
248	    };
249	    if !seed.status.success() {
250	        let _ = broker.kill();
251	        let _ = broker.wait();
252	        panic!(
253	            "PRECONDITION FAILED: api seed failed: {}",
254	            String::from_utf8_lossy(&seed.stderr)
255	        );
256	    }
257
258	    // The persistent live `api listen` — marks the perch status=online (live-capable
259	    // because the manifest declares [session.psyche_init]) then blocks. The harness
260	    // keeps this process alive for the session's lifetime.
261	    let listen_log = home.path().join("listen.stderr.log");
262	    let listen_log_file = std::fs::File::create(&listen_log).expect("create listen stderr log");
263	    let mut listen: Child = Command::new(&spt_bin)
264	        .no_window()
265	        .args([
266	            "api",
267	            "--adapter",
268	            "mocklive",
269	            "--manifest",
270	            &mp,
271	            "listen",
272	            id,
273	            "--parent-pid",
274	            &pid,
275	        ])
276	        .env("SPT_HOME", home.path())
277	        .stdout(Stdio::null())
278	        .stderr(Stdio::from(listen_log_file))
279	        .spawn()
280	        .expect("spawn the persistent api listen (harness-hosted live agent)");
281
282	    // Confirm the establish→online actually took: the Self perch carries
283	    // status=online (written by cmd_listen). Poll briefly — `listen` writes it
284	    // right after BOUND.
285	    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
286	    let online = {
287	        let deadline = Instant::now() + Duration::from_secs(15);
288	        loop {
289	            let st = spt_store::info::read_info(&self_perch).and_then(|i| i.status);
290	            if st.as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
291	                break true;
292	            }
293	            if Instant::now() >= deadline {
294	                break false;
295	            }
296	            std::thread::sleep(Duration::from_millis(100));
297	        }
298	    };
299	    if !online {
300	        kill_pid(listen.id());
301	        let _ = listen.wait();
302	        let _ = broker.kill();
303	        let _ = broker.wait();
304	        panic!(
305	            "PRECONDITION FAILED: the persistent `api listen` never marked the perch online.\n\
306	             listen stderr:\n{}",
307	            std::fs::read_to_string(&listen_log).unwrap_or_default()
308	        );
309	    }
310
311	    // ── (5) THE ASSERTION: the REAL daemon brain must host the Psyche. The
312	    //    disk-observable proxy is the nested `<id>-psyche` perch's info.json
313	    //    appearing (spawn_and_bind writes it AFTER a successful psyche spawn; a
314	    //    spawn failure logs LIVEHOST_PSYCHE_FAIL and writes nothing). Bounded 25s
315	    //    poll — on a buggy tree this SHOULD time out. ──
316	    let psyche_perch = perch::resolve_perch_path(
317	        &format!("{id}-psyche"),
318	        ParentHint::Explicit(id),
319	    );
320	    let deadline = Instant::now() + Duration::from_secs(25);
321	    let mut hosted = false;
322	    while Instant::now() < deadline {
323	        if spt_store::info::read_info(&psyche_perch).is_some() {
324	            hosted = true;
325	            break;
326	        }
327	        std::thread::sleep(Duration::from_millis(200));
328	    }
329
330	    // ── (5b) RESIDENCY CONFIRM (v0.8.2, REQ-HAZARD-LIVEHOST-NONRESIDENT): the host
331	    //    is not just "spawned Ok" — it must STILL be resident past the grace window.
332	    //    Wait past the residency grace (~5s) + one reconcile tick (~5s) so the brain's
333	    //    `confirm_residency_or_unhost` has run on the resident psyche at least once,
334	    //    then prove BOTH: the nested `<id>-psyche` pid is STILL ALIVE (the same probe
335	    //    the impl uses — read_pid → is_process_alive) AND the parent carries no
336	    //    `psyche_host_error`. With a fast-exiting psyche this would be a Residency::Failed
337	    //    (pid dead, parent stamped); the LONG-LIVED `ready` fixture keeps it Resident.
338	    //    Bounded busy-wait (no fixed sleep): poll a fixed window, capture the last pid. ──
339	    let resident_deadline = Instant::now() + Duration::from_secs(13);
340	    while Instant::now() < resident_deadline {
341	        std::thread::sleep(Duration::from_millis(200));
342	    }
343	    let psyche_pid = spt_store::info::read_pid(&psyche_perch);
344	    let psyche_alive = psyche_pid.map(spt_store::proc::is_process_alive).unwrap_or(false);
345
346	    // Capture diagnostics BEFORE teardown so the report has the real markers.
347	    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
348	    let listen_stderr = std::fs::read_to_string(&listen_log).unwrap_or_default();
349
350	    // The harness-reachable psyche-host-health field (v0.8.1/v0.8.2): on the RESIDENT
351	    // success path the live perch must carry NO `psyche_host_error` — a confirmed-live
352	    // agent shows clean. Read it BEFORE teardown.
353	    // [int->REQ-HAZARD-LIVEHOST-BOOT-RACE]
354	    // [int->REQ-HAZARD-LIVEHOST-NONRESIDENT]
355	    let phe = spt_store::info::read_info(&self_perch).and_then(|i| i.psyche_host_error);
356
357	    // Always surface the captured markers (the reproduction evidence).
358	    eprintln!(
359	        "=== DIAGNOSTIC: hosted={hosted} psyche_pid={psyche_pid:?} psyche_alive={psyche_alive} \
360	         phe={phe:?} brain_pid={brain_pid} ==="
361	    );
362	    eprintln!("=== brain stderr (BRAIN_UP / LIVEHOST_PSYCHE / LIVEHOST_PSYCHE_NONRESIDENT) ===\n{brain_stderr}");
363	    eprintln!("=== listen stderr ===\n{listen_stderr}");
364	    eprintln!(
365	        "=== psyche perch info.json present: {} (path {}) ===",
366	        psyche_perch.join("info.json").exists(),
367	        psyche_perch.display()
368	    );
369
370	    // ── (6) Reap everything we spawned — scoped to OUR pids only, NEVER a
371	    //    machine-wide reap (a live broker shares this box). The resident psyche pid
372	    //    is read from the nested perch info.json (the only pid we know for it). ──
373	    if let Some(pp) = psyche_pid {
374	        kill_pid(pp);
375	    }
376	    kill_pid(listen.id());
377	    let _ = listen.wait();
378	    kill_pid(brain_pid);
379	    let _ = broker.kill();
380	    let _ = broker.wait();
381
382	    assert!(
383	        hosted,
384	        "REQ-DAEMON-1: the REAL daemon brain must host the Psyche for a net-less ONLINE \
385	         live endpoint (the nested `{id}-psyche` perch must appear) within 25s — it did NOT.\n\
386	         === brain stderr (BRAIN_UP / LIVEHOST_PSYCHE / LIVEHOST_PSYCHE_FAIL land here) ===\n{}\n\
387	         === listen stderr ===\n{}",
388	        brain_stderr, listen_stderr,
389	    );
390	    // RESIDENT proof: the long-lived psyche is still alive past grace + one tick, so
391	    // the residency confirm classified it Resident (not Failed) — the Resident arm of
392	    // confirm_residency_or_unhost is exercised end-to-end, no timing race.
393	    assert!(
394	        psyche_alive,
395	        "REQ-HAZARD-LIVEHOST-NONRESIDENT: a healthy long-lived psyche must still be ALIVE \
396	         past the residency grace + one reconcile tick (Resident, not falsely torn down) — \
397	         the nested `{id}-psyche` pid {psyche_pid:?} read DEAD/missing.\n\
398	         === brain stderr ===\n{brain_stderr}\n=== listen stderr ===\n{listen_stderr}",
399	    );
400	    assert!(
401	        phe.is_none(),
402	        "REQ-HAZARD-LIVEHOST-NONRESIDENT: a CONFIRMED-RESIDENT live perch must carry NO \
403	         psyche_host_error (the Resident arm clears it; a slow-but-alive psyche must NOT \
404	         false-fail) — got {phe:?}",
405	    );
406	}
407

//! v0.8.1 boot-race REPRODUCER + v0.8.2 RESIDENT-path proof (REQ-DAEMON-1 /
//! REQ-HAZARD-LIVEHOST-NONRESIDENT) — a harness-hosted live agent goes
//! `status=online` and the REAL daemon brain hosts its Psyche on a net-less /
//! unpaired node, the host CONFIRMS RESIDENCY (the psyche stays alive past the
//! grace window), and the parent perch shows clean (no `psyche_host_error`).
//!
//! This is the full-stack production-path E2E that the existing analogs stop short
//! of (doyle's mandate):
//!
//!   - `daemon_lifecycle_real_brain.rs` runs `run_brain` on a THREAD, uses a NOOP
//!     `cmd /C rem` psyche (a shell builtin that always resolves — it never
//!     exercises install-dir binary resolution), and HAND-WRITES the online perch.
//!   - This test launches the REAL `spt daemon run` broker PROCESS (which spawns a
//!     real supervised `spt daemon brain` CHILD), drives the REAL `api seed` +
//!     `api listen` establish→online path, and uses a REAL psyche binary referenced
//!     by a BARE program token that lives ONLY in the adapter install dir — so the
//!     daemon-host site MUST resolve it via REQ-INSTALL-11 install-dir resolution,
//!     not PATH.
//!
//! v0.8.2 (F-010) RESIDENCY note — the psyche fixture is now LONG-LIVED. Post-F-010
//! a fast-exiting psyche is a FAILED host by definition (residency confirm fires a
//! `psyche_host_error` once past the ~5s grace), so the old `psychebin --version`
//! fixture would have made THIS positive test a hollow pass that only held by racing
//! the brain before the grace — flake-prone on the slow shared kitsubito runner, and
//! it left the RESIDENT arm with ZERO E2E coverage. The fixture is therefore a copy
//! of the real `spt` binary invoked by the BARE token as `psychebin ready
//! <throwaway-id>` — `spt ready` (non-`--once`) is a persistent message listener that
//! BLOCKS in an infinite `recv()` loop needing only `SPT_HOME` (which the test sets),
//! so the spawned psyche STAYS ALIVE until we reap it. (Chosen over a bespoke sleeper
//! binary: it reuses the same real `spt` copy + bare-token install-dir resolution the
//! negative tests use — full REQ-INSTALL-11 fidelity, zero new fixture machinery, and
//! cross-platform on both CI runners.) The test then WAITS past the grace + one
//! reconcile tick and asserts the residency CONFIRMED: the nested `<id>-psyche` pid is
//! STILL ALIVE and the parent carries NO `psyche_host_error`.
//!
//! The disk-observable proxy for "the brain hosted the Psyche" is the nested
//! `<id>-psyche` perch's `info.json` appearing (mirrors `daemon_lifecycle_real_brain.rs`).
//! On a buggy tree this never appears within the bounded deadline.
//!
//! Bounded by design: every captured `spt` call has a deadline and every host/residency
//! wait is a bounded polling loop (<=30s), so a reproduced failure is a clean assert,
//! never a hang. All spawned processes (broker + supervised brain + listen + the
//! resident psyche) are reaped — scoped to OUR pids only, never a machine-wide reap.
// [int->REQ-DAEMON-1]
// [int->REQ-HAZARD-LIVEHOST-NONRESIDENT]

use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

/// Best-effort cross-platform kill of a pid tree (mirrors the other E2E helpers).
/// Scoped to a SINGLE pid we spawned — never a machine-wide reap (a live broker
/// shares this box).
fn kill_pid(pid: u32) {
    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .no_window()
        .args(["/PID", &pid.to_string(), "/F", "/T"])
        .output();
    #[cfg(unix)]
    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
}

/// `Command::output()` with a deadline, run off-thread — bounds a hang into a
/// clean failure (the `live_firsthost_e2e` pattern).
fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete within its deadline")
        .expect("run spt")
}

/// Read the brain `pid` out of a `brain.ready` body (self-describing JSON since
/// D6-1b). The come-up proof: the broker spawned a brain child.
fn ready_pid(body: &str) -> Option<u32> {
    serde_json::from_str::<serde_json::Value>(body)
        .ok()?
        .get("pid")?
        .as_u64()
        .map(|p| p as u32)
}

/// Poll `brain.ready` for a pid, up to `budget`. `None` if it never appears.
fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Ok(s) = std::fs::read_to_string(path) {
            if let Some(pid) = ready_pid(&s) {
                return Some(pid);
            }
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

#[test]
fn netless_online_live_agent_is_hosted_by_the_real_daemon_brain() {
    // ── (1) Isolated SPT_HOME, NET-LESS / UNPAIRED: we create no subnet and run no
    //    pairing. The whole defect window is "online live agent on a node with no
    //    net". One test in this binary owns the process-global SPT_HOME; children
    //    are passed it explicitly via .env() anyway. ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    // Force the truest NET-LESS / UNPAIRED condition: a CORRUPT node.key makes
    // `nodeid::load_or_create()` error, so `Daemon::run` computes `node_hex = None`
    // → it NEVER calls `try_start_net` and arms NO net-retry (daemon.rs:153-161,
    // "NODE_KEY_FAIL: identity unavailable — broker runs net-less, no retry"). The
    // brain's `net-status` therefore never reports `enabled`, so BRAIN_NET_CONSUMERS_UP
    // never fires — the exact net-starved node perri hit. (A corrupt key is NEVER
    // auto-regenerated by design — nodeid.rs module docs.)
    let identity_dir = home.path().join("identity");
    std::fs::create_dir_all(&identity_dir).unwrap();
    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let exe_suffix = spt_bin
        .extension()
        .map(|e| format!(".{}", e.to_string_lossy()))
        .unwrap_or_default();

    // ── (2) A live-capable adapter in COPY mode whose [session.psyche_init].command
    //    is a BARE program token (`psychebin`) naming a REAL psyche binary that
    //    ships in the adapter's install dir (= the registered `source_dir`). The
    //    fixture is a copy of the real `spt` binary invoked as `psychebin ready
    //    <throwaway-id>` — a LONG-LIVED psyche (`spt ready`, non-`--once`, BLOCKS in
    //    an infinite recv() loop until killed; needs only SPT_HOME). Post-F-010 a
    //    fast-exiting psyche is a FAILED host, so the resident-positive proof REQUIRES
    //    a psyche that stays alive past the residency grace. The bare token is NOT on
    //    PATH, so spawn_psyche can ONLY resolve it via REQ-INSTALL-11 install-dir
    //    resolution. ──
    let src = perch::spt_home().join("srcs").join("mocklive");
    std::fs::create_dir_all(&src).unwrap();

    // The real psyche binary fixture (a copy of `spt`), by a BARE name, IN the
    // install dir only — invoked below as the long-lived `psychebin ready ...`.
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the real psyche binary fixture");
    assert!(
        psyche_bin.is_file(),
        "the psyche binary fixture must exist in the install dir"
    );

    // The drops dir (commune/signoff watch) — irrelevant to the host proof but the
    // manifest needs a [session] block.
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");

    // Command is the BARE token `psychebin` + a BLOCKING subcommand. NOT an
    // absolute path, NOT a shell builtin: it resolves only if install-dir
    // resolution fires. `ready <throwaway-id>` makes the spawned psyche a
    // persistent message listener that blocks until reaped — a LONG-LIVED resident
    // (multi-word value is single-token here, so it survives the fill order). The
    // throwaway id is namespaced so it never collides with the live agent's id.
    let manifest_toml = format!(
        "[adapter]\nname=\"mocklive\"\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='psychebin ready bootrace-resident-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    let manifest_path = src.join("manifest.toml");
    std::fs::write(&manifest_path, &manifest_toml).unwrap();
    // Register in COPY mode (no [update] avenue ⇒ copy). The record's `source_dir`
    // is `src` — the install dir the brain resolves `psychebin` against.
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // Sanity: install-dir resolution of the bare token DOES find the fixture (so a
    // failure later is the WIRING not the fixture).
    let resolved =
        spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mocklive").map(|(rec, _)| {
            spt_runtime::runtime::resolve_program_in_dir("psychebin", Path::new(&rec.source_dir))
        });
    assert!(
        resolved
            .as_ref()
            .map(|p| Path::new(p).is_file())
            .unwrap_or(false),
        "install-dir resolution must find the bare psychebin in source_dir (got {resolved:?})"
    );

    // ── (3) Launch the REAL daemon broker PROCESS. `spt daemon run` is the broker;
    //    it spawns a supervised `spt daemon brain` CHILD. Redirect the broker's
    //    stderr to a log file in the tempdir — the brain child inherits the broker's
    //    stderr (spawn_brain_child sets no stdio), so BRAIN_UP / LIVEHOST_PSYCHE /
    //    LIVEHOST_PSYCHE_FAIL (all eprintln! to the brain's stderr) land here. ──
    let brain_log = home.path().join("brain.stderr.log");
    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
    let mut broker: Child = Command::new(&spt_bin)
        .no_window()
        .args(["daemon", "run"])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(brain_log_file))
        .spawn()
        .expect("spawn spt daemon run (broker process)");

    // Come-up: the broker spawned a brain child that signalled ready. This is the
    // precondition — if the brain never comes up, the host claim is moot.
    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
    {
        Some(p) => p,
        None => {
            let _ = broker.kill();
            let _ = broker.wait();
            panic!(
                "PRECONDITION FAILED: the real broker never spawned a ready brain child \
                 (brain.ready absent). Brain stderr:\n{}",
                std::fs::read_to_string(&brain_log).unwrap_or_default()
            );
        }
    };

    // ── (4) The REAL harness establish→online path: `api seed` then `api listen`.
    //    Seed keyed by THIS process's pid (alive — passes the recycle guard). The
    //    listen is the PERSISTENT (non --once) form: it marks the perch online and
    //    then BLOCKS in the relay loop until killed (the real harness leaves it
    //    running). Run it as a captured child whose stderr we keep, so we can see
    //    its BOUND/READY markers and reap it. ──
    let id = "bootrace-agent";
    let pid = std::process::id().to_string();
    let mp = manifest_path.to_string_lossy().to_string();

    let seed = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args([
                "api",
                "--adapter",
                "mocklive",
                "seed",
                "--pid",
                &pid,
                "--session-id",
                "sid-bootrace",
            ])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(30))
    };
    if !seed.status.success() {
        let _ = broker.kill();
        let _ = broker.wait();
        panic!(
            "PRECONDITION FAILED: api seed failed: {}",
            String::from_utf8_lossy(&seed.stderr)
        );
    }

    // The persistent live `api listen` — marks the perch status=online (live-capable
    // because the manifest declares [session.psyche_init]) then blocks. The harness
    // keeps this process alive for the session's lifetime.
    let listen_log = home.path().join("listen.stderr.log");
    let listen_log_file = std::fs::File::create(&listen_log).expect("create listen stderr log");
    let mut listen: Child = Command::new(&spt_bin)
        .no_window()
        .args([
            "api",
            "--adapter",
            "mocklive",
            "--manifest",
            &mp,
            "listen",
            id,
            "--parent-pid",
            &pid,
        ])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(listen_log_file))
        .spawn()
        .expect("spawn the persistent api listen (harness-hosted live agent)");

    // Confirm the establish→online actually took: the Self perch carries
    // status=online (written by cmd_listen). Poll briefly — `listen` writes it
    // right after BOUND.
    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    let online = {
        let deadline = Instant::now() + Duration::from_secs(15);
        loop {
            let st = spt_store::info::read_info(&self_perch).and_then(|i| i.status);
            if st.as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
                break true;
            }
            if Instant::now() >= deadline {
                break false;
            }
            std::thread::sleep(Duration::from_millis(100));
        }
    };
    if !online {
        kill_pid(listen.id());
        let _ = listen.wait();
        let _ = broker.kill();
        let _ = broker.wait();
        panic!(
            "PRECONDITION FAILED: the persistent `api listen` never marked the perch online.\n\
             listen stderr:\n{}",
            std::fs::read_to_string(&listen_log).unwrap_or_default()
        );
    }

    // ── (5) THE ASSERTION: the REAL daemon brain must host the Psyche. The
    //    disk-observable proxy is the nested `<id>-psyche` perch's info.json
    //    appearing (spawn_and_bind writes it AFTER a successful psyche spawn; a
    //    spawn failure logs LIVEHOST_PSYCHE_FAIL and writes nothing). Bounded 25s
    //    poll — on a buggy tree this SHOULD time out. ──
    let psyche_perch = perch::resolve_perch_path(
        &format!("{id}-psyche"),
        ParentHint::Explicit(id),
    );
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut hosted = false;
    while Instant::now() < deadline {
        if spt_store::info::read_info(&psyche_perch).is_some() {
            hosted = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(200));
    }

    // ── (5b) RESIDENCY CONFIRM (v0.8.2, REQ-HAZARD-LIVEHOST-NONRESIDENT): the host
    //    is not just "spawned Ok" — it must STILL be resident past the grace window.
    //    Wait past the residency grace (~5s) + one reconcile tick (~5s) so the brain's
    //    `confirm_residency_or_unhost` has run on the resident psyche at least once,
    //    then prove BOTH: the nested `<id>-psyche` pid is STILL ALIVE (the same probe
    //    the impl uses — read_pid → is_process_alive) AND the parent carries no
    //    `psyche_host_error`. With a fast-exiting psyche this would be a Residency::Failed
    //    (pid dead, parent stamped); the LONG-LIVED `ready` fixture keeps it Resident.
    //    Bounded busy-wait (no fixed sleep): poll a fixed window, capture the last pid. ──
    let resident_deadline = Instant::now() + Duration::from_secs(13);
    while Instant::now() < resident_deadline {
        std::thread::sleep(Duration::from_millis(200));
    }
    let psyche_pid = spt_store::info::read_pid(&psyche_perch);
    let psyche_alive = psyche_pid.map(spt_store::proc::is_process_alive).unwrap_or(false);

    // Capture diagnostics BEFORE teardown so the report has the real markers.
    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    let listen_stderr = std::fs::read_to_string(&listen_log).unwrap_or_default();

    // The harness-reachable psyche-host-health field (v0.8.1/v0.8.2): on the RESIDENT
    // success path the live perch must carry NO `psyche_host_error` — a confirmed-live
    // agent shows clean. Read it BEFORE teardown.
    // [int->REQ-HAZARD-LIVEHOST-BOOT-RACE]
    // [int->REQ-HAZARD-LIVEHOST-NONRESIDENT]
    let phe = spt_store::info::read_info(&self_perch).and_then(|i| i.psyche_host_error);

    // Always surface the captured markers (the reproduction evidence).
    eprintln!(
        "=== DIAGNOSTIC: hosted={hosted} psyche_pid={psyche_pid:?} psyche_alive={psyche_alive} \
         phe={phe:?} brain_pid={brain_pid} ==="
    );
    eprintln!("=== brain stderr (BRAIN_UP / LIVEHOST_PSYCHE / LIVEHOST_PSYCHE_NONRESIDENT) ===\n{brain_stderr}");
    eprintln!("=== listen stderr ===\n{listen_stderr}");
    eprintln!(
        "=== psyche perch info.json present: {} (path {}) ===",
        psyche_perch.join("info.json").exists(),
        psyche_perch.display()
    );

    // ── (6) Reap everything we spawned — scoped to OUR pids only, NEVER a
    //    machine-wide reap (a live broker shares this box). The resident psyche pid
    //    is read from the nested perch info.json (the only pid we know for it). ──
    if let Some(pp) = psyche_pid {
        kill_pid(pp);
    }
    kill_pid(listen.id());
    let _ = listen.wait();
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();

    assert!(
        hosted,
        "REQ-DAEMON-1: the REAL daemon brain must host the Psyche for a net-less ONLINE \
         live endpoint (the nested `{id}-psyche` perch must appear) within 25s — it did NOT.\n\
         === brain stderr (BRAIN_UP / LIVEHOST_PSYCHE / LIVEHOST_PSYCHE_FAIL land here) ===\n{}\n\
         === listen stderr ===\n{}",
        brain_stderr, listen_stderr,
    );
    // RESIDENT proof: the long-lived psyche is still alive past grace + one tick, so
    // the residency confirm classified it Resident (not Failed) — the Resident arm of
    // confirm_residency_or_unhost is exercised end-to-end, no timing race.
    assert!(
        psyche_alive,
        "REQ-HAZARD-LIVEHOST-NONRESIDENT: a healthy long-lived psyche must still be ALIVE \
         past the residency grace + one reconcile tick (Resident, not falsely torn down) — \
         the nested `{id}-psyche` pid {psyche_pid:?} read DEAD/missing.\n\
         === brain stderr ===\n{brain_stderr}\n=== listen stderr ===\n{listen_stderr}",
    );
    assert!(
        phe.is_none(),
        "REQ-HAZARD-LIVEHOST-NONRESIDENT: a CONFIRMED-RESIDENT live perch must carry NO \
         psyche_host_error (the Resident arm clears it; a slow-but-alive psyche must NOT \
         false-fail) — got {phe:?}",
    );
}


I have the harness model. Write the B5 int — a cold-start daemon with a hand-stamped **stale** online+controllable perch (no broker session), asserting B5 offlines it at boot and never revives a Psyche:


Write

//! B5 boot-liveness-gate int (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE) — a COLD
//! daemon start after an unclean stop must NOT revive a phantom Psyche for a
//! dead-but-online-latched perch.
//!
//! The scenario the operator hit: a prior daemon life left an spt-hosted
//! (`controllable==Some(true)`) `live_agent` perch stamped `status=online` on disk,
//! then the daemon died uncleanly (the one-way latch was never cleared). On the
//! NEXT cold start the broker has NO session for it (the harness is long gone), yet
//! the old `reconcile_once` would spawn a Psyche per online live_agent perch — N
//! phantom psyches for N dead perches.
//!
//! B5 closes it by running the B2 `reconcile_hosted_liveness` from BOOT tick 1 (no
//! grace): the brain queries the broker's live session set (empty at a cold start),
//! marks the sessionless controllable perch OFFLINE, and `reconcile_once` then skips
//! hosting it. This test stands up the REAL `spt daemon run` broker + supervised
//! brain over a net-less node, hand-stamps exactly such a stale perch (the cold-start
//! leftover — there is deliberately NO `api listen`/`bind`, so the broker hosts no
//! session for it), and asserts BOTH: the perch is flipped OFFLINE at boot AND the
//! nested `{id}-psyche` perch NEVER appears (no phantom revival).
//!
//! The adapter is genuinely live-capable (a resolvable psyche binary in its install
//! dir), so the ONLY reason no Psyche is hosted is the B5 gate — not a host failure.
//! Bounded by design; all spawned pids are reaped scoped (never a machine-wide reap).
// [int->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]

use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

/// Scoped pid-tree kill (mirrors the sibling livehost E2Es) — never machine-wide.
fn kill_pid(pid: u32) {
    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .no_window()
        .args(["/PID", &pid.to_string(), "/F", "/T"])
        .output();
    #[cfg(unix)]
    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
}

/// Poll `brain.ready` for the brain pid (self-describing JSON) up to `budget`.
fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Ok(s) = std::fs::read_to_string(path) {
            if let Some(pid) = serde_json::from_str::<serde_json::Value>(&s)
                .ok()
                .and_then(|v| v.get("pid").and_then(|p| p.as_u64()))
            {
                return Some(pid as u32);
            }
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

#[test]
fn cold_start_does_not_revive_a_sessionless_online_latched_perch() {
    // ── (1) Isolated, NET-LESS SPT_HOME (a corrupt node.key forces node_hex=None →
    //    the broker never tries net; the brain still boots + hosts). ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    let identity_dir = home.path().join("identity");
    std::fs::create_dir_all(&identity_dir).unwrap();
    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let exe_suffix = spt_bin
        .extension()
        .map(|e| format!(".{}", e.to_string_lossy()))
        .unwrap_or_default();

    // ── (2) A genuinely live-capable adapter in COPY mode: its psyche_init names a
    //    BARE `psychebin` token that resolves (a copy of the real `spt`, run as the
    //    long-lived `ready` listener) — so absent the B5 gate, reconcile_once WOULD
    //    host a Psyche. That makes "no Psyche hosted" a proof of the GATE, not of a
    //    host failure. ──
    let src = perch::spt_home().join("srcs").join("mocklive");
    std::fs::create_dir_all(&src).unwrap();
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"mocklive\"\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='psychebin ready bootgate-phantom-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // ── (3) The STALE cold-start leftover: an spt-hosted (controllable) live_agent
    //    perch stamped status=online on disk, with NO broker session backing it (we
    //    run no listen/bind). This is the dead-but-latched phantom B5 must NOT revive. ──
    let id = "bootgate-phantom";
    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&self_perch).unwrap();
    let mut rec =
        spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-phantom", "live_agent");
    rec.adapter = Some("mocklive".to_string());
    rec.controllable = Some(true);
    spt_store::info::write_info(&self_perch, &rec).unwrap();
    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();

    // ── (4) Launch the REAL daemon broker PROCESS (spawns a supervised brain child).
    //    Brain stderr → a log file (LIVEHOST_PSYCHE / LIVENESS_RECONCILE_OFFLINE land
    //    here). ──
    let brain_log = home.path().join("brain.stderr.log");
    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
    let mut broker: Child = Command::new(&spt_bin)
        .no_window()
        .args(["daemon", "run"])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(brain_log_file))
        .spawn()
        .expect("spawn spt daemon run (broker process)");

    let brain_pid =
        match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30)) {
            Some(p) => p,
            None => {
                let _ = broker.kill();
                let _ = broker.wait();
                panic!(
                    "PRECONDITION FAILED: the real broker never spawned a ready brain child.\n{}",
                    std::fs::read_to_string(&brain_log).unwrap_or_default()
                );
            }
        };

    // ── (5) THE ASSERTION: the brain's boot-tick reconcile clears the latch. Poll the
    //    perch status flip to OFFLINE (the B5 boot-gate signal — reconcile_hosted_liveness
    //    is the only thing that offlines a controllable perch at boot). Bounded. ──
    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut offlined = false;
    while Instant::now() < deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_OFFLINE) {
            offlined = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(200));
    }

    // ── (5b) NO PHANTOM: the nested `{id}-psyche` perch must NEVER appear (host_one
    //    writes it only on a Psyche spawn). Give a host-attempt window past the offline
    //    flip to be sure no late tick revived it. ──
    let psyche_perch =
        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    let settle = Instant::now() + Duration::from_secs(8);
    while Instant::now() < settle {
        std::thread::sleep(Duration::from_millis(200));
    }
    let psyche_appeared = spt_store::info::read_info(&psyche_perch).is_some();
    let psyche_pid = spt_store::info::read_pid(&psyche_perch);

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!(
        "=== DIAGNOSTIC: offlined={offlined} final_status={:?} psyche_appeared={psyche_appeared} \
         psyche_pid={psyche_pid:?} brain_pid={brain_pid} ===",
        status()
    );
    eprintln!("=== brain stderr ===\n{brain_stderr}");

    // ── (6) Reap scoped — a phantom psyche should not exist, but if the bug revived
    //    one, kill it so the test never leaks. ──
    if let Some(pp) = psyche_pid {
        kill_pid(pp);
    }
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();

    assert!(
        offlined,
        "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE: the boot-tick reconcile must clear the \
         status=online latch for a sessionless controllable perch (no live broker session) — \
         it stayed {:?} after 25s.\n=== brain stderr ===\n{brain_stderr}",
        status()
    );
    assert!(
        !psyche_appeared,
        "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE: a dead-but-latched perch must NOT have its \
         Psyche revived at boot — the nested `{id}-psyche` perch appeared (phantom revived).\n\
         === brain stderr ===\n{brain_stderr}",
    );
}


<owl_messages>
<owl_message from="doyle" priority="high">Decision: B5 scope is CORRECT as you have it — leave the runtime-psyche-reap OUT of B5 (B5 = prevent boot revival, full stop). Build B5 impl+unit+int as planned + the cold-start int. I verified your read myself (HostedLife = {stop,thread,hosted_at}, stop_host only flags+joins the driver thread; the psyche pid lives on the {id}-psyche perch, used by residency at livehost.rs:401).

The runtime "stop_host leaves the detached psyche PROCESS alive until daemon-stop/Breap" gap is REAL and production-reachable (endpoint stop / mid-life agent death → driver stops, psyche lingers), but it's a SEPARATE concern from both B5 and B4-as-written (B4 = rehydrate lifecycle on brain restart, not reap). I'm raising it to the operator as a scope call (small new REQ: stop_host reads the {id}-psyche perch pid + kills it, vs accept Breap-at-daemon-stop as the bound). Don't fold it into B5/B4 unless the operator greenlights — keep your waves clean. Proceed with B5; ping at wave-4 ready.</owl_message>
</owl_messages>

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


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt\tests\livehost_bootgate_e2e.rs (file state is current in your context — no need to Read it back)

//! B5 boot-liveness-gate int (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE) — a COLD
//! daemon start after an unclean stop must NOT revive a phantom Psyche for a
//! dead-but-online-latched perch.
//!
//! The scenario the operator hit: a prior daemon life left an spt-hosted
//! (`controllable==Some(true)`) `live_agent` perch stamped `status=online` on disk,
//! then the daemon died uncleanly (the one-way latch was never cleared). On the
//! NEXT cold start the broker has NO session for it (the harness is long gone), yet
//! the old `reconcile_once` would spawn a Psyche per online live_agent perch — N
//! phantom psyches for N dead perches.
//!
//! B5 closes it by running the B2 `reconcile_hosted_liveness` from BOOT tick 1 (no
//! grace): the brain queries the broker's live session set (empty at a cold start),
//! marks the sessionless controllable perch OFFLINE, and `reconcile_once` then skips
//! hosting it. This test stands up the REAL `spt daemon run` broker + supervised
//! brain over a net-less node, hand-stamps exactly such a stale perch (the cold-start
//! leftover — there is deliberately NO `api listen`/`bind`, so the broker hosts no
//! session for it), and asserts BOTH: the perch is flipped OFFLINE at boot AND the
//! nested `{id}-psyche` perch NEVER appears (no phantom revival).
//!
//! The adapter is genuinely live-capable (a resolvable psyche binary in its install
//! dir), so the ONLY reason no Psyche is hosted is the B5 gate — not a host failure.
//! Bounded by design; all spawned pids are reaped scoped (never a machine-wide reap).
// [int->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]

use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

/// Scoped pid-tree kill (mirrors the sibling livehost E2Es) — never machine-wide.
fn kill_pid(pid: u32) {
    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .no_window()
        .args(["/PID", &pid.to_string(), "/F", "/T"])
        .output();
    #[cfg(unix)]
    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
}

/// Poll `brain.ready` for the brain pid (self-describing JSON) up to `budget`.
fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Ok(s) = std::fs::read_to_string(path) {
            if let Some(pid) = serde_json::from_str::<serde_json::Value>(&s)
                .ok()
                .and_then(|v| v.get("pid").and_then(|p| p.as_u64()))
            {
                return Some(pid as u32);
            }
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

#[test]
fn cold_start_does_not_revive_a_sessionless_online_latched_perch() {
    // ── (1) Isolated, NET-LESS SPT_HOME (a corrupt node.key forces node_hex=None →
    //    the broker never tries net; the brain still boots + hosts). ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    let identity_dir = home.path().join("identity");
    std::fs::create_dir_all(&identity_dir).unwrap();
    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let exe_suffix = spt_bin
        .extension()
        .map(|e| format!(".{}", e.to_string_lossy()))
        .unwrap_or_default();

    // ── (2) A genuinely live-capable adapter in COPY mode: its psyche_init names a
    //    BARE `psychebin` token that resolves (a copy of the real `spt`, run as the
    //    long-lived `ready` listener) — so absent the B5 gate, reconcile_once WOULD
    //    host a Psyche. That makes "no Psyche hosted" a proof of the GATE, not of a
    //    host failure. ──
    let src = perch::spt_home().join("srcs").join("mocklive");
    std::fs::create_dir_all(&src).unwrap();
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"mocklive\"\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='psychebin ready bootgate-phantom-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // ── (3) The STALE cold-start leftover: an spt-hosted (controllable) live_agent
    //    perch stamped status=online on disk, with NO broker session backing it (we
    //    run no listen/bind). This is the dead-but-latched phantom B5 must NOT revive. ──
    let id = "bootgate-phantom";
    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&self_perch).unwrap();
    let mut rec =
        spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-phantom", "live_agent");
    rec.adapter = Some("mocklive".to_string());
    rec.controllable = Some(true);
    spt_store::info::write_info(&self_perch, &rec).unwrap();
    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();

    // ── (4) Launch the REAL daemon broker PROCESS (spawns a supervised brain child).
    //    Brain stderr → a log file (LIVEHOST_PSYCHE / LIVENESS_RECONCILE_OFFLINE land
    //    here). ──
    let brain_log = home.path().join("brain.stderr.log");
    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
    let mut broker: Child = Command::new(&spt_bin)
        .no_window()
        .args(["daemon", "run"])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(brain_log_file))
        .spawn()
        .expect("spawn spt daemon run (broker process)");

    let brain_pid =
        match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30)) {
            Some(p) => p,
            None => {
                let _ = broker.kill();
                let _ = broker.wait();
                panic!(
                    "PRECONDITION FAILED: the real broker never spawned a ready brain child.\n{}",
                    std::fs::read_to_string(&brain_log).unwrap_or_default()
                );
            }
        };

    // ── (5) THE ASSERTION: the brain's boot-tick reconcile clears the latch. Poll the
    //    perch status flip to OFFLINE (the B5 boot-gate signal — reconcile_hosted_liveness
    //    is the only thing that offlines a controllable perch at boot). Bounded. ──
    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut offlined = false;
    while Instant::now() < deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_OFFLINE) {
            offlined = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(200));
    }

    // ── (5b) NO PHANTOM: the nested `{id}-psyche` perch must NEVER appear (host_one
    //    writes it only on a Psyche spawn). Give a host-attempt window past the offline
    //    flip to be sure no late tick revived it. ──
    let psyche_perch =
        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    let settle = Instant::now() + Duration::from_secs(8);
    while Instant::now() < settle {
        std::thread::sleep(Duration::from_millis(200));
    }
    let psyche_appeared = spt_store::info::read_info(&psyche_perch).is_some();
    let psyche_pid = spt_store::info::read_pid(&psyche_perch);

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!(
        "=== DIAGNOSTIC: offlined={offlined} final_status={:?} psyche_appeared={psyche_appeared} \
         psyche_pid={psyche_pid:?} brain_pid={brain_pid} ===",
        status()
    );
    eprintln!("=== brain stderr ===\n{brain_stderr}");

    // ── (6) Reap scoped — a phantom psyche should not exist, but if the bug revived
    //    one, kill it so the test never leaks. ──
    if let Some(pp) = psyche_pid {
        kill_pid(pp);
    }
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();

    assert!(
        offlined,
        "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE: the boot-tick reconcile must clear the \
         status=online latch for a sessionless controllable perch (no live broker session) — \
         it stayed {:?} after 25s.\n=== brain stderr ===\n{brain_stderr}",
        status()
    );
    assert!(
        !psyche_appeared,
        "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE: a dead-but-latched perch must NOT have its \
         Psyche revived at boot — the nested `{id}-psyche` perch appeared (phantom revived).\n\
         === brain stderr ===\n{brain_stderr}",
    );
}


doyle: B5 scope confirmed correct (runtime-psyche-reap stays OUT — he's raising it to the operator as a separate REQ). B5 impl+unit+int are written. ACK doyle (protocol), then run the seam bar:


Bash

cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
ACK — B5 scope locked (boot-revival prevention only; runtime-psyche-reap stays out, your separate-REQ call to the operator is the right home for it). Thanks for verifying the HostedLife read.

B5 BUILT (impl+unit+int activated):
- impl: lifted the LIVENESS_RECONCILE_BOOT_GRACE skip in spawn_live_host → reconcile_hosted_liveness now runs from boot tick 1 (BEFORE reconcile_once), reusing the B2 fn (no second liveness notion); removed the now-dead const + booted Instant; same controllable==Some(true) gate; None-skip preserved. Tagged both [impl->HOSTED-LIVENESS-RECONCILE] + [impl->LIVEHOST-BOOT-LIVENESS-GATE] at the site.
- unit (livehost.rs): boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host — composes reconcile_hosted_liveness(empty for phantom / session for livee) THEN reconcile_once; asserts phantom offlined + NOT hosted (set.len excludes it), livee stays online + hosted. Proves the gate is the session, not a blanket block.
- int (new livehost_bootgate_e2e.rs): real cold-start `spt daemon run`, hand-stamped STALE online+controllable perch (no broker session, genuinely live-capable adapter w/ resolvable psychebin) → asserts perch flips OFFLINE at boot AND the {id}-psyche perch NEVER appears (no phantom revival; not hosted because of the gate, not a host failure).

Running the seam bar now (clippy --workspace -D warnings + traceable + new units/int + full sweep incl bootgate, serial in target-seam). Ping you at wave-4 ready with results + the 3-way matrix confirmation.
EOF
echo "sent rc=$?"

Ack doyle + B5 build summary


➜ SENT:doyle
sent rc=0


CARGO_TARGET_DIR=target-seam cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -8; echo "CLIPPY_RC=${PIPESTATUS[0]}"

Workspace clippy after B5


Command running in background with ID: bbxprcmpz. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bbxprcmpz.output. You will be notified when it completes. To check interim output, use Read on that file path.


Monitor

B5 workspace clippy result

until grep -qE "CLIPPY_RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bbxprcmpz.output" 2>/dev/null; do sleep 5; done; grep -E "warning:|error\[|error:|CLIPPY_RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bbxprcmpz.output" | tail -10


Monitor started (task bq91lwlb1, timeout 420000ms). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply.


Awaiting clippy. Once green I'll run traceable + the B5 unit + the new int + the full seam sweep, all serial.


<task-notification>
<task-id>bbxprcmpz</task-id>
<tool-use-id>toolu_01Kcdp3uaaEoiDdVtPpoSv8M</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bbxprcmpz.output</output-file>
<status>completed</status>
<summary>Background command "Workspace clippy after B5" completed (exit code 0)</summary>
</task-notification>


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


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


<task-notification>
<task-id>bq91lwlb1</task-id>
<summary>Monitor event: "B5 workspace clippy result"</summary>
<event>CLIPPY_RC=0</event>
</task-notification>


<task-notification>
<task-id>bq91lwlb1</task-id>
<tool-use-id>toolu_01LkTxDZsgosWgb3wsaUprit</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bq91lwlb1.output</output-file>
<status>completed</status>
<summary>Monitor "B5 workspace clippy result" stream ended</summary>
</task-notification>


Bash

grep -E "warning:|error\[|error:|CLIPPY_RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bbxprcmpz.output" | tail; echo "=== traceable ==="; cd "C:/Users/decid/Documents/projects/spt-core"; traceable-reqs check >/tmp/tr5.txt 2>&1; echo "TR_EXIT=$?"; grep -E "complete,|BOOT-LIVENESS-GATE" /tmp/tr5.txt

Confirm clippy + run traceable


CLIPPY_RC=0
=== traceable ===
TR_EXIT=0
244 requirements: 244 complete, 0 incomplete, 0 findings
  [OK] REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE  required: [impl, unit, int]  stages: -doc +impl +unit +int


Prompt:
<task-notification>
<task-id>bq91lwlb1</task-id>
<summary>Monitor event: "B5 workspace clippy result"</summary>
<event>CLIPPY_RC=0</event>
</task-notification>


Prompt:
<task-notification>
<task-id>bq91lwlb1</task-id>
<tool-use-id>toolu_01LkTxDZsgosWgb3wsaUprit</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bq91lwlb1.output</output-file>
<status>completed</status>
<summary>Monitor "B5 workspace clippy result" stream ended</summary>
</task-notification>


Clippy RC=0, traceable EXIT=0 (B5 +impl+unit+int). Run the livehost lib units (B5 unit + B2 still-green), serial:


CARGO_TARGET_DIR=target-seam cargo test -p spt-daemon --lib -- livehost 2>&1 | grep -E "test result:|FAILED|error\[|error:|panicked|boot_gate|pull_liveness|pull_reconcile"; echo "RC=${PIPESTATUS[0]}"

Run livehost lib units (B5 + B2)


Command running in background with ID: bpednxg7j. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bpednxg7j.output. You will be notified when it completes. To check interim output, use Read on that file path.


Monitor

livehost lib unit result (B5+B2)

until grep -qE "RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bpednxg7j.output" 2>/dev/null; do sleep 4; done; grep -E "test result:|FAILED|error|panicked|boot_gate|pull_|RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bpednxg7j.output"


Monitor started (task bvlok209h, timeout 300000ms). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply.


<task-notification>
<task-id>bpednxg7j</task-id>
<tool-use-id>toolu_011VjNYFXEsLg7GJ51q8SY1D</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bpednxg7j.output</output-file>
<status>completed</status>
<summary>Background command "Run livehost lib units (B5 + B2)" completed (exit code 0)</summary>
</task-notification>


Awaiting livehost lib units.


<task-notification>
<task-id>bvlok209h</task-id>
<summary>Monitor event: "livehost lib unit result (B5+B2)"</summary>
<event>test livehost::tests::boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host ... ok
test livehost::tests::pull_liveness_marks_sessionless_spt_hosted_offline_only ... ok
test livehost::tests::pull_reconcile_offlines_perch_when_broker_session_dies ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 313 filtered out; finished in 1.95s
RC=0</event>
</task-notification>


<task-notification>
<task-id>bvlok209h</task-id>
<tool-use-id>toolu_016szmW2rdRr71VnhX9XWNud</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bvlok209h.output</output-file>
<status>completed</status>
<summary>Monitor "livehost lib unit result (B5+B2)" stream ended</summary>
</task-notification>


Bash

Read livehost lib unit result


test livehost::tests::boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host ... ok
test livehost::tests::pull_liveness_marks_sessionless_spt_hosted_offline_only ... ok
test livehost::tests::pull_reconcile_offlines_perch_when_broker_session_dies ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 313 filtered out; finished in 1.95s
RC=0


Prompt:
<task-notification>
<task-id>bvlok209h</task-id>
<summary>Monitor event: "livehost lib unit result (B5+B2)"</summary>
<event>test livehost::tests::boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host ... ok
test livehost::tests::pull_liveness_marks_sessionless_spt_hosted_offline_only ... ok
test livehost::tests::pull_reconcile_offlines_perch_when_broker_session_dies ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 313 filtered out; finished in 1.95s
RC=0</event>
</task-notification>


Prompt:
<task-notification>
<task-id>bvlok209h</task-id>
<tool-use-id>toolu_016szmW2rdRr71VnhX9XWNud</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bvlok209h.output</output-file>
<status>completed</status>
<summary>Monitor "livehost lib unit result (B5+B2)" stream ended</summary>
</task-notification>


10 livehost lib units green (B5 + B2). Now the spt livehost E2Es + the new bootgate int (regression check + new int proof), single serial invocation:


CARGO_TARGET_DIR=target-seam cargo test -p spt --test livehost_bootgate_e2e --test livehost_bootrace_e2e --test livehost_nonresident_e2e --test live_firsthost_e2e --test live_bind_firsthost_e2e --test livehost_psyche_fail_e2e 2>&1 | grep -E "test result:|FAILED|error\[|error:|panicked|Running |cold_start_does"; echo "RC=${PIPESTATUS[0]}"

Run spt livehost E2Es + new bootgate int


Command running in background with ID: bnag4a18a. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bnag4a18a.output. You will be notified when it completes. To check interim output, use Read on that file path.


Monitor

spt livehost E2Es + bootgate int result

until grep -qE "RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bnag4a18a.output" 2>/dev/null; do sleep 6; done; grep -E "test result:|FAILED|error|panicked|cold_start_does|RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bnag4a18a.output"


Monitor started (task bg84agyk2, timeout 600000ms). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply.


Awaiting the livehost E2E + bootgate result (the regression-sensitive + new-int run).


<task-notification>
<task-id>bnag4a18a</task-id>
<tool-use-id>toolu_01UBkDCk81GKS7wsva4CAYVv</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bnag4a18a.output</output-file>
<status>completed</status>
<summary>Background command "Run spt livehost E2Es + new bootgate int" completed (exit code 0)</summary>
</task-notification>


<task-notification>
<task-id>bg84agyk2</task-id>
<summary>Monitor event: "spt livehost E2Es + bootgate int result"</summary>
<event>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.87s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.15s
test cold_start_does_not_revive_a_sessionless_online_latched_perch ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 20.72s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.35s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.81s
RC=0</event>
</task-notification>


<task-notification>
<task-id>bg84agyk2</task-id>
<tool-use-id>toolu_01LJ2hVgxZpnEMoFJ9wZ1Cx6</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bg84agyk2.output</output-file>
<status>completed</status>
<summary>Monitor "spt livehost E2Es + bootgate int result" stream ended</summary>
</task-notification>


Bash

Read E2E + bootgate result


test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.87s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.15s
test cold_start_does_not_revive_a_sessionless_online_latched_perch ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 20.72s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.35s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.81s
RC=0


Prompt:
<task-notification>
<task-id>bg84agyk2</task-id>
<summary>Monitor event: "spt livehost E2Es + bootgate int result"</summary>
<event>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.87s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.15s
test cold_start_does_not_revive_a_sessionless_online_latched_perch ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 20.72s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 13.35s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.81s
RC=0</event>
</task-notification>


Prompt:
<task-notification>
<task-id>bg84agyk2</task-id>
<tool-use-id>toolu_01LJ2hVgxZpnEMoFJ9wZ1Cx6</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bg84agyk2.output</output-file>
<status>completed</status>
<summary>Monitor "spt livehost E2Es + bootgate int result" stream ended</summary>
</task-notification>


All 6 green: the **new bootgate int** (`cold_start_does_not_revive...`) + all 5 existing livehost E2Es — **no regression** (confirms firsthost/bind-firsthost in-proc→None-skip, listen→Some(false)-exempt). Now the rest of the full sweep (serial):


CARGO_TARGET_DIR=target-seam cargo test -p spt-daemon --test daemon_lifecycle_real_brain --test broker --test handoff --test attach --test brain_swap --test resume --test daemon_e2e 2>&1 | grep -E "test result:|FAILED|error\[|error:|panicked"; echo "RC=${PIPESTATUS[0]}"

Run spt-daemon full seam sweep


Command running in background with ID: bq7dmg1qz. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bq7dmg1qz.output. You will be notified when it completes. To check interim output, use Read on that file path.


Monitor

spt-daemon full seam sweep result

until grep -qE "RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bq7dmg1qz.output" 2>/dev/null; do sleep 6; done; grep -E "test result:|FAILED|error|panicked|RC=" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bq7dmg1qz.output"


Monitor started (task bvyxh0tlw, timeout 600000ms). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply.


Awaiting the final seam-sweep batch.


<task-notification>
<task-id>bq7dmg1qz</task-id>
<tool-use-id>toolu_013oe935UCQL6bgaXnF72S25</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bq7dmg1qz.output</output-file>
<status>completed</status>
<summary>Background command "Run spt-daemon full seam sweep" completed (exit code 0)</summary>
</task-notification>


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


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


<task-notification>
<task-id>bvyxh0tlw</task-id>
<summary>Monitor event: "spt-daemon full seam sweep result"</summary>
<event>test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.98s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.22s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.42s
RC=0</event>
</task-notification>


<task-notification>
<task-id>bvyxh0tlw</task-id>
<tool-use-id>toolu_01Emc4jjBSUiaUp29QLrvpmT</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bvyxh0tlw.output</output-file>
<status>completed</status>
<summary>Monitor "spt-daemon full seam sweep result" stream ended</summary>
</task-notification>


Bash

Read full sweep result


test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.98s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.22s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.42s
RC=0


Prompt:
<task-notification>
<task-id>bvyxh0tlw</task-id>
<summary>Monitor event: "spt-daemon full seam sweep result"</summary>
<event>test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.98s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.22s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.42s
RC=0</event>
</task-notification>


Prompt:
<task-notification>
<task-id>bvyxh0tlw</task-id>
<tool-use-id>toolu_01Emc4jjBSUiaUp29QLrvpmT</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bvyxh0tlw.output</output-file>
<status>completed</status>
<summary>Monitor "spt-daemon full seam sweep result" stream ended</summary>
</task-notification>


Full sweep green (24 tests). **B5 seam bar complete:** clippy `-D warnings` RC=0 · traceable EXIT=0 (+impl+unit+int) · livehost lib units 10 · livehost E2Es + bootgate int 6 (no regression) · spt-daemon sweep 24. Commit B5:


cd "C:/Users/decid/Documents/projects/spt-core" && git add traceable-reqs.toml crates/spt-daemon/src/livehost.rs crates/spt/tests/livehost_bootgate_e2e.rs && git commit -q -F - <<'EOF'
v0.12.0 wave 4 — B5 REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE

A cold daemon start after an unclean stop revived a phantom Psyche per
status=online live_agent perch (reconcile_once spawned one for every online perch
without verifying the harness is alive — N phantoms for N dead-but-latched perches).
Activated [impl,unit,int].

FIX (boot-gate arm of the B2 reconcile — reuse, no second liveness notion): lift the
LIVENESS_RECONCILE_BOOT_GRACE skip so reconcile_hosted_liveness runs from BOOT tick 1,
BEFORE reconcile_once. A sessionless controllable==Some(true) perch is marked OFFLINE
at boot, so reconcile_once's start-side never revives its Psyche. The grace was safe to
drop for this class: an spt-hosted agent runs IN a broker PTY, so its broker session
PRECEDES the online mark — online+controllable+sessionless is only ever a dead phantom
(cold-start leftover) or a brain-restart survivor that IS still in the session set.
None-skip preserved (run_brain connects before livehost → tick-1 query returns the real
set; None only on a broker hiccup → never mass-offline). Relay Some(false)/legacy None
stay exempt via the existing controllable gate. Removed the now-dead grace const + booted.

3-way phantom matrix now closed: (i) child-dead mid-life -> B2 pull-reconcile;
(ii) boot-revive after unclean stop -> B5 boot-gate (this); (iii) dead {id}-psyche ->
residency-confirm.

unit (livehost.rs): boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host
— reconcile_hosted_liveness(empty for the phantom / session for the live one) THEN
reconcile_once; phantom offlined + not hosted, live one stays online + hosted (gate is
the session, not a blanket block). int (livehost_bootgate_e2e.rs): real cold-start
`spt daemon run` + a hand-stamped stale online+controllable perch (no broker session,
genuinely live-capable adapter) -> perch flips OFFLINE at boot AND the {id}-psyche perch
NEVER appears (no phantom revival).

No regression: the existing livehost E2Es use `api listen` (controllable=Some(false),
exempt) or call reconcile_once in-process with no broker (None-skip) — none host a
sessionless controllable perch.

Seam bar: clippy --workspace --all-targets -D warnings clean; traceable EXIT=0; livehost
units + livehost E2Es + bootgate int + full sweep (daemon_lifecycle_real_brain, broker,
handoff, attach, brain_swap, resume, daemon_e2e) green — serial in target-seam.

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

Commit B5


warning: in the working copy of 'crates/spt/tests/livehost_bootgate_e2e.rs', LF will be replaced by CRLF the next time Git touches it
9e338f1 v0.12.0 wave 4 — B5 REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE


Committed @9e338f1. Ping doyle for the wave-4 gate:


cd "C:/Users/decid/Documents/projects/spt-core" && $OWL send doyle <<'EOF'
WAVE 4 (B5) READY @9e338f1 — for your gate.

IMPL (reuse, no fork): lifted the LIVENESS_RECONCILE_BOOT_GRACE skip in spawn_live_host so reconcile_hosted_liveness runs from boot tick 1, BEFORE reconcile_once. Sessionless controllable==Some(true) perch → OFFLINE at boot → reconcile_once skips hosting → no phantom revival. Removed the dead const + booted Instant. None-skip + controllable gate preserved. Tagged [impl] both HOSTED-LIVENESS-RECONCILE + LIVEHOST-BOOT-LIVENESS-GATE at the site.

WHY dropping the grace is safe (the load-bearing argument): an spt-hosted live agent runs IN a broker PTY → its broker session PRECEDES the online mark (daemon spawns the PTY, THEN the hook binds). So online+controllable+sessionless is ONLY ever (a) a cold-start dead phantom or (b) a brain-restart survivor that IS in the session set. (a) gets offlined, (b) stays. The grace only ever delayed correct behavior.

3-WAY MATRIX (confirmed): (i) child-dead mid-life → B2 pull-reconcile; (ii) boot-revive after unclean stop → B5 boot-gate; (iii) dead {id}-psyche → residency-confirm. Distinct mechanisms, no overlap.

REGRESSION CHECK (the bind-firsthost question, resolved): the existing livehost E2Es DON'T host a sessionless controllable perch, so B5 touches none —
- bootrace/nonresident/psyche_fail: `api listen` → controllable=Some(false) → exempt.
- firsthost/bind-firsthost: call reconcile_once IN-PROCESS (no `daemon run`, no broker) → spawn_live_host (where the boot-gate lives) never runs → unaffected. (And query would be None-skip anyway.)

SEAM BAR (serial, target-seam): clippy --workspace --all-targets -D warnings clean; traceable EXIT=0 (B5 +impl+unit+int); livehost lib units 10/10 (boot_gate + B2); livehost E2Es 5/5 + NEW bootgate int (cold_start_does_not_revive_a_sessionless_online_latched_perch) green; full sweep (daemon_lifecycle_real_brain, broker, handoff, attach, brain_swap, resume, daemon_e2e) 24 green.

Stack now 7 ahead of main (026e9b4, 0c1e59e, 9e338f1 + the wave2/B2/mint below). NOT pushed. Your gate → wave 5 (B4 brain-restart-lifecycle-rehydrate, the deepest).
EOF
echo "sent rc=$?"

Ping doyle for wave-4 gate


➜ SENT:doyle
sent rc=0


B5 built, seam-barred green, committed @9e338f1, handed to doyle for the wave-4 gate. Update the milestone memory + then hold for the gate (wave 5 = B4 follows).


Edit

**PROGRESS: B2 GATE-PASS @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e (grandchild reap proven, ShellExecute-breakaway escape caught+fixed). WAVE 4 (B5 boot-liveness-gate) RELEASED to todlando.** Remaining: B5 (wave 4, in progress) → B4 (wave 5, deepest). Stack 6 ahead of main, NOT pushed. Then deployah → v0.12.0 MINOR.

**PROGRESS: B2 GATE-PASS @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e. WAVE 4 (B5 boot-liveness-gate) BUILT+seam-green @9e338f1, awaiting doyle gate.** Remaining: B4 (wave 5, deepest, after B5 gate). Stack 7 ahead of main, NOT pushed. Then deployah → v0.12.0 MINOR.

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