1	//! v0.8.1 NEGATIVE E2E (REQ-HAZARD-LIVEHOST-BOOT-RACE) — a harness-hosted live
2	//! agent that goes `status=online` but whose Psyche FAILS to host (the psyche
3	//! binary is ABSENT from the adapter install dir) must leave a HARNESS-REACHABLE
4	//! diagnostic on the perch's `info.json` (`psyche_host_error`), not merely an
5	//! `eprintln!` on the brain's invisible stderr.
6	//!
7	//! This is the companion to `livehost_bootrace_e2e.rs` (the POSITIVE case). It
8	//! drives the SAME real production path — `spt daemon run` broker → supervised
9	//! `spt daemon brain` CHILD → `api seed` + persistent `api listen` (online) — on
10	//! a net-less / unpaired node, but the adapter's `[session.psyche_init].command`
11	//! is a BARE token (`psychebin`) with NO matching binary anywhere (not in the
12	//! install dir, not on PATH). So `spawn_psyche` fails every reconcile tick, and
13	//! the brain's `host_one` must stamp the perch `info.json` with a
14	//! `psyche_host_error` (reason non-empty, attempts >= 1) — the previously-silent
15	//! failure is now diagnosable from PERCH STATE.
16	//!
17	//! Lives in its own test binary (so it owns the process-global `SPT_HOME`
18	//! independently of the positive test). Bounded by design: every captured `spt`
19	//! call has a deadline and the failure-stamp wait is a bounded poll, so a
20	//! regression is a clean assert, never a hang. All spawned processes (broker +
21	//! supervised brain + listen) are reaped — scoped to OUR pids only, never a
22	//! machine-wide reap (a live broker shares this box).
23	// [int->REQ-HAZARD-LIVEHOST-BOOT-RACE]
24
25	use std::path::{Path, PathBuf};
26	use std::process::{Child, Command, Output, Stdio};
27	use std::time::{Duration, Instant};
28
29	mod common;
30	use common::CommandNoWindowExt;
31
32	use spt_store::perch::{self, ParentHint};
33
34	/// Best-effort cross-platform kill of a single pid tree we spawned — NEVER a
35	/// machine-wide reap (a live broker shares this box).
36	fn kill_pid(pid: u32) {
37	    #[cfg(windows)]
38	    let _ = Command::new("taskkill")
39	        .no_window()
40	        .args(["/PID", &pid.to_string(), "/F", "/T"])
41	        .output();
42	    #[cfg(unix)]
43	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
44	}
45
46	/// `Command::output()` with a deadline, run off-thread — bounds a hang into a
47	/// clean failure.
48	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
49	    let (tx, rx) = std::sync::mpsc::channel();
50	    std::thread::spawn(move || {
51	        let _ = tx.send(cmd.output());
52	    });
53	    rx.recv_timeout(deadline)
54	        .expect("captured spt call must complete within its deadline")
55	        .expect("run spt")
56	}
57
58	/// Read the brain `pid` out of a `brain.ready` body (self-describing JSON).
59	fn ready_pid(body: &str) -> Option<u32> {
60	    serde_json::from_str::<serde_json::Value>(body)
61	        .ok()?
62	        .get("pid")?
63	        .as_u64()
64	        .map(|p| p as u32)
65	}
66
67	/// Poll `brain.ready` for a pid, up to `budget`. `None` if it never appears.
68	fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
69	    let deadline = Instant::now() + budget;
70	    while Instant::now() < deadline {
71	        if let Ok(s) = std::fs::read_to_string(path) {
72	            if let Some(pid) = ready_pid(&s) {
73	                return Some(pid);
74	            }
75	        }
76	        std::thread::sleep(Duration::from_millis(50));
77	    }
78	    None
79	}
80
81	#[test]
82	fn online_live_agent_with_missing_psyche_binary_stamps_a_harness_reachable_error() {
83	    // ── (1) Isolated SPT_HOME, NET-LESS / UNPAIRED (corrupt node.key → no net,
84	    //    no retry — the exact net-starved condition). One test in this binary owns
85	    //    the process-global SPT_HOME; children get it via .env() anyway. ──
86	    let home = tempfile::tempdir().unwrap();
87	    std::env::set_var("SPT_HOME", home.path());
88
89	    let identity_dir = home.path().join("identity");
90	    std::fs::create_dir_all(&identity_dir).unwrap();
91	    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();
92
93	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
94
95	    // ── (2) A live-capable adapter in COPY mode whose [session.psyche_init].command
96	    //    is a BARE token (`psychebin`) for which NO binary exists — NOT in the
97	    //    install dir (we deliberately do NOT stage it), NOT on PATH. So spawn_psyche
98	    //    MUST fail every reconcile tick: the precise "psyche binary absent from its
99	    //    install dir" defect this hazard is about. ──
100	    let src = perch::spt_home().join("srcs").join("mockfail");
101	    std::fs::create_dir_all(&src).unwrap();
102
103	    // (deliberately NO psychebin fixture staged in `src` — that is the defect.)
104	    let drops = home.path().join("drops");
105	    std::fs::create_dir_all(&drops).unwrap();
106	    let dir = drops.to_string_lossy().replace('\\', "/");
107
108	    let manifest_toml = format!(
109	        "[adapter]\nname=\"mockfail\"\nkind=\"harness\"\nversion=\"1\"\n\
110	         min_spt_core_version=\"0\"\n\n\
111	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
112	         [session.psyche_init]\ncommand='psychebin --version'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
113	    );
114	    let manifest_path = src.join("manifest.toml");
115	    std::fs::write(&manifest_path, &manifest_toml).unwrap();
116	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
117
118	    // Sanity: install-dir resolution of the bare token must FAIL to find a file
119	    // (so the spawn failure later is the missing binary, not the wiring).
120	    let resolved =
121	        spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mockfail").map(|(rec, _)| {
122	            spt_runtime::runtime::resolve_program_in_dir("psychebin", Path::new(&rec.source_dir))
123	        });
124	    assert!(
125	        resolved
126	            .as_ref()
127	            .map(|p| !Path::new(p).is_file())
128	            .unwrap_or(true),
129	        "the bare psychebin must NOT resolve to a real file (got {resolved:?})"
130	    );
131
132	    // ── (3) Launch the REAL daemon broker PROCESS (spawns a supervised brain CHILD).
133	    //    Redirect broker stderr to a log file (the brain child inherits it). ──
134	    let brain_log = home.path().join("brain.stderr.log");
135	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
136	    let mut broker: Child = Command::new(&spt_bin)
137	        .no_window()
138	        .args(["daemon", "run"])
139	        .env("SPT_HOME", home.path())
140	        .stdout(Stdio::null())
141	        .stderr(Stdio::from(brain_log_file))
142	        .spawn()
143	        .expect("spawn spt daemon run (broker process)");
144
145	    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
146	    {
147	        Some(p) => p,
148	        None => {
149	            let _ = broker.kill();
150	            let _ = broker.wait();
151	            panic!(
152	                "PRECONDITION FAILED: the real broker never spawned a ready brain child.\n{}",
153	                std::fs::read_to_string(&brain_log).unwrap_or_default()
154	            );
155	        }
156	    };
157
158	    // ── (4) The REAL harness establish→online path: `api seed` then persistent
159	    //    `api listen` (marks the perch online, then blocks). ──
160	    let id = "psychefail-agent";
161	    let pid = std::process::id().to_string();
162	    let mp = manifest_path.to_string_lossy().to_string();
163
164	    let seed = {
165	        let mut cmd = Command::new(&spt_bin);
166	        cmd.no_window()
167	            .args([
168	                "api",
169	                "--adapter",
170	                "mockfail",
171	                "seed",
172	                "--pid",
173	                &pid,
174	                "--session-id",
175	                "sid-psychefail",
176	            ])
177	            .env("SPT_HOME", home.path());
178	        output_bounded(cmd, Duration::from_secs(30))
179	    };
180	    if !seed.status.success() {
181	        let _ = broker.kill();
182	        let _ = broker.wait();
183	        panic!(
184	            "PRECONDITION FAILED: api seed failed: {}",
185	            String::from_utf8_lossy(&seed.stderr)
186	        );
187	    }
188
189	    let listen_log = home.path().join("listen.stderr.log");
190	    let listen_log_file = std::fs::File::create(&listen_log).expect("create listen stderr log");
191	    let mut listen: Child = Command::new(&spt_bin)
192	        .no_window()
193	        .args([
194	            "api",
195	            "--adapter",
196	            "mockfail",
197	            "--manifest",
198	            &mp,
199	            "listen",
200	            id,
201	            "--parent-pid",
202	            &pid,
203	        ])
204	        .env("SPT_HOME", home.path())
205	        .stdout(Stdio::null())
206	        .stderr(Stdio::from(listen_log_file))
207	        .spawn()
208	        .expect("spawn the persistent api listen (harness-hosted live agent)");
209
210	    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
211	    let online = {
212	        let deadline = Instant::now() + Duration::from_secs(15);
213	        loop {
214	            let st = spt_store::info::read_info(&self_perch).and_then(|i| i.status);
215	            if st.as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
216	                break true;
217	            }
218	            if Instant::now() >= deadline {
219	                break false;
220	            }
221	            std::thread::sleep(Duration::from_millis(100));
222	        }
223	    };
224	    if !online {
225	        kill_pid(listen.id());
226	        let _ = listen.wait();
227	        let _ = broker.kill();
228	        let _ = broker.wait();
229	        panic!(
230	            "PRECONDITION FAILED: the persistent `api listen` never marked the perch online.\n\
231	             listen stderr:\n{}",
232	            std::fs::read_to_string(&listen_log).unwrap_or_default()
233	        );
234	    }
235
236	    // ── (5) THE ASSERTION: with the psyche binary MISSING, the brain's host_one
237	    //    must stamp the perch info.json with a `psyche_host_error` (reason
238	    //    non-empty, attempts >= 1) — the failure is now HARNESS-DIAGNOSABLE on the
239	    //    perch, not just on the brain's stderr. The reconcile cadence is 5s, so a
240	    //    ~6s tick must have fired; bounded 25s poll. The nested `<id>-psyche` perch
241	    //    must NEVER appear (a spawn failure writes no psyche perch). ──
242	    let psyche_perch =
243	        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
244	    let deadline = Instant::now() + Duration::from_secs(25);
245	    let mut stamped: Option<spt_store::info::PsycheHostError> = None;
246	    while Instant::now() < deadline {
247	        if let Some(e) = spt_store::info::read_info(&self_perch).and_then(|i| i.psyche_host_error) {
248	            stamped = Some(e);
249	            break;
250	        }
251	        std::thread::sleep(Duration::from_millis(200));
252	    }
253
254	    // Status must STILL be online (liveness is daemon-authoritative; the failure
255	    // stamp NEVER touches it). The psyche perch must NOT exist (spawn failed).
256	    let status_after = spt_store::info::read_info(&self_perch).and_then(|i| i.status);
257	    let psyche_perch_present = spt_store::info::read_info(&psyche_perch).is_some();
258
259	    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
260	    let listen_stderr = std::fs::read_to_string(&listen_log).unwrap_or_default();
261
262	    eprintln!("=== DIAGNOSTIC: stamped={stamped:?} status_after={status_after:?} brain_pid={brain_pid} ===");
263	    eprintln!("=== brain stderr (LIVEHOST_PSYCHE_FAIL lands here) ===\n{brain_stderr}");
264	    eprintln!("=== listen stderr ===\n{listen_stderr}");
265
266	    // ── (6) Reap everything we spawned — scoped to OUR pids only. ──
267	    kill_pid(listen.id());
268	    let _ = listen.wait();
269	    kill_pid(brain_pid);
270	    let _ = broker.kill();
271	    let _ = broker.wait();
272
273	    let stamped = stamped.unwrap_or_else(|| {
274	        panic!(
275	            "REQ-HAZARD-LIVEHOST-BOOT-RACE: an ONLINE live endpoint whose Psyche fails to host \
276	             (psyche binary absent) MUST stamp a harness-reachable psyche_host_error on its perch \
277	             info.json within 25s — it did NOT (the failure stayed silent on brain stderr).\n\
278	             === brain stderr ===\n{brain_stderr}\n=== listen stderr ===\n{listen_stderr}"
279	        )
280	    });
281	    assert!(
282	        !stamped.reason.is_empty(),
283	        "the stamped psyche_host_error must carry a non-empty reason (got {stamped:?})"
284	    );
285	    assert!(
286	        stamped.attempts >= 1,
287	        "the stamped psyche_host_error must carry attempts >= 1 (got {stamped:?})"
288	    );
289	    assert_eq!(
290	        status_after.as_deref(),
291	        Some(spt_store::liveness::STATUS_ONLINE),
292	        "status stays daemon-authoritative online — the failure stamp never touches it"
293	    );
294	    assert!(
295	        !psyche_perch_present,
296	        "a failed psyche spawn must write NO nested `{id}-psyche` perch"
297	    );
298	}
299

//! v0.8.1 NEGATIVE E2E (REQ-HAZARD-LIVEHOST-BOOT-RACE) — a harness-hosted live
//! agent that goes `status=online` but whose Psyche FAILS to host (the psyche
//! binary is ABSENT from the adapter install dir) must leave a HARNESS-REACHABLE
//! diagnostic on the perch's `info.json` (`psyche_host_error`), not merely an
//! `eprintln!` on the brain's invisible stderr.
//!
//! This is the companion to `livehost_bootrace_e2e.rs` (the POSITIVE case). It
//! drives the SAME real production path — `spt daemon run` broker → supervised
//! `spt daemon brain` CHILD → `api seed` + persistent `api listen` (online) — on
//! a net-less / unpaired node, but the adapter's `[session.psyche_init].command`
//! is a BARE token (`psychebin`) with NO matching binary anywhere (not in the
//! install dir, not on PATH). So `spawn_psyche` fails every reconcile tick, and
//! the brain's `host_one` must stamp the perch `info.json` with a
//! `psyche_host_error` (reason non-empty, attempts >= 1) — the previously-silent
//! failure is now diagnosable from PERCH STATE.
//!
//! Lives in its own test binary (so it owns the process-global `SPT_HOME`
//! independently of the positive test). Bounded by design: every captured `spt`
//! call has a deadline and the failure-stamp wait is a bounded poll, so a
//! regression is a clean assert, never a hang. All spawned processes (broker +
//! supervised brain + listen) are reaped — scoped to OUR pids only, never a
//! machine-wide reap (a live broker shares this box).
// [int->REQ-HAZARD-LIVEHOST-BOOT-RACE]

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 single pid tree 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.
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).
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 online_live_agent_with_missing_psyche_binary_stamps_a_harness_reachable_error() {
    // ── (1) Isolated SPT_HOME, NET-LESS / UNPAIRED (corrupt node.key → no net,
    //    no retry — the exact net-starved condition). One test in this binary owns
    //    the process-global SPT_HOME; children get it via .env() anyway. ──
    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"));

    // ── (2) A live-capable adapter in COPY mode whose [session.psyche_init].command
    //    is a BARE token (`psychebin`) for which NO binary exists — NOT in the
    //    install dir (we deliberately do NOT stage it), NOT on PATH. So spawn_psyche
    //    MUST fail every reconcile tick: the precise "psyche binary absent from its
    //    install dir" defect this hazard is about. ──
    let src = perch::spt_home().join("srcs").join("mockfail");
    std::fs::create_dir_all(&src).unwrap();

    // (deliberately NO psychebin fixture staged in `src` — that is the defect.)
    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=\"mockfail\"\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 --version'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    let manifest_path = src.join("manifest.toml");
    std::fs::write(&manifest_path, &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // Sanity: install-dir resolution of the bare token must FAIL to find a file
    // (so the spawn failure later is the missing binary, not the wiring).
    let resolved =
        spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mockfail").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(true),
        "the bare psychebin must NOT resolve to a real file (got {resolved:?})"
    );

    // ── (3) Launch the REAL daemon broker PROCESS (spawns a supervised brain CHILD).
    //    Redirect broker stderr to a log file (the brain child inherits it). ──
    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()
            );
        }
    };

    // ── (4) The REAL harness establish→online path: `api seed` then persistent
    //    `api listen` (marks the perch online, then blocks). ──
    let id = "psychefail-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",
                "mockfail",
                "seed",
                "--pid",
                &pid,
                "--session-id",
                "sid-psychefail",
            ])
            .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)
        );
    }

    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",
            "mockfail",
            "--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)");

    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: with the psyche binary MISSING, the brain's host_one
    //    must stamp the perch info.json with a `psyche_host_error` (reason
    //    non-empty, attempts >= 1) — the failure is now HARNESS-DIAGNOSABLE on the
    //    perch, not just on the brain's stderr. The reconcile cadence is 5s, so a
    //    ~6s tick must have fired; bounded 25s poll. The nested `<id>-psyche` perch
    //    must NEVER appear (a spawn failure writes no psyche perch). ──
    let psyche_perch =
        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut stamped: Option<spt_store::info::PsycheHostError> = None;
    while Instant::now() < deadline {
        if let Some(e) = spt_store::info::read_info(&self_perch).and_then(|i| i.psyche_host_error) {
            stamped = Some(e);
            break;
        }
        std::thread::sleep(Duration::from_millis(200));
    }

    // Status must STILL be online (liveness is daemon-authoritative; the failure
    // stamp NEVER touches it). The psyche perch must NOT exist (spawn failed).
    let status_after = spt_store::info::read_info(&self_perch).and_then(|i| i.status);
    let psyche_perch_present = spt_store::info::read_info(&psyche_perch).is_some();

    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();

    eprintln!("=== DIAGNOSTIC: stamped={stamped:?} status_after={status_after:?} brain_pid={brain_pid} ===");
    eprintln!("=== brain stderr (LIVEHOST_PSYCHE_FAIL lands here) ===\n{brain_stderr}");
    eprintln!("=== listen stderr ===\n{listen_stderr}");

    // ── (6) Reap everything we spawned — scoped to OUR pids only. ──
    kill_pid(listen.id());
    let _ = listen.wait();
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();

    let stamped = stamped.unwrap_or_else(|| {
        panic!(
            "REQ-HAZARD-LIVEHOST-BOOT-RACE: an ONLINE live endpoint whose Psyche fails to host \
             (psyche binary absent) MUST stamp a harness-reachable psyche_host_error on its perch \
             info.json within 25s — it did NOT (the failure stayed silent on brain stderr).\n\
             === brain stderr ===\n{brain_stderr}\n=== listen stderr ===\n{listen_stderr}"
        )
    });
    assert!(
        !stamped.reason.is_empty(),
        "the stamped psyche_host_error must carry a non-empty reason (got {stamped:?})"
    );
    assert!(
        stamped.attempts >= 1,
        "the stamped psyche_host_error must carry attempts >= 1 (got {stamped:?})"
    );
    assert_eq!(
        status_after.as_deref(),
        Some(spt_store::liveness::STATUS_ONLINE),
        "status stays daemon-authoritative online — the failure stamp never touches it"
    );
    assert!(
        !psyche_perch_present,
        "a failed psyche spawn must write NO nested `{id}-psyche` perch"
    );
}


Prompt:
<task-notification>
<task-id>b20v41a9b</task-id>
<tool-use-id>toolu_0176dxk8LTmZ6rD8vV3eNRc6</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\b20v41a9b.output</output-file>
<status>completed</status>
<summary>Background command "Check PR #18 state + CI rollup" completed (exit code 0)</summary>
</task-notification>


This is the NEGATIVE test. Need the re-spun POSITIVE one — `livehost_bootrace_e2e.rs`. Find it + read CI result.


n1-gate (self-hosted, Windows, hfenduleam)

v0.8.2: argv-element template fill (F-009) + Psyche residency confirm (F-010)


CI in-progress both runners on 0aad7b9 (n1-gate Linux PASS, tests running). Read the re-spun positive test while CI runs.


<owl_messages>
<owl_message from="doyle-psyche" priority="normal"><EVENT type="echo_commune" from="doyle-psyche" timestamp="2026-06-16T17:55:17-07:00" machine="HFENDULEAM" project="spt-core" branch="v0.8.2-templating-residency" head_sha="0aad7b946b1c291e9dae8dc768c78498287b5f94" head_subject="v0.8.2 re-spin: resident-positive E2E + residency reason reword (gater n…" note="Echo commune brief — auto-fired">&lt;live-context&gt;
DELTA — v0.8.2 GATED conditionally → todlando RE-SPUN → awaiting my RE-GATE (session wrap, resume next session).

Since the last download, the v0.8.2 arc moved from &quot;awaiting PR-ready&quot; to &quot;re-spin PR-ready, awaiting re-gate&quot;:
- todlando submitted PR #18 (v0.8.2, F-009 + F-010). I GATED it — code-read PASS on F-009 (fill_template_tokens: tokenize-then-fill, value=1 argv element, errors preserved, callers rewired) and F-010 residency (DIRECT is_process_alive pid-probe NOT the masking is_perch_alive, clear-moved-to-Resident-arm, de-phantom + cooldown, pure classify_residency unit). Negative real-daemon E2E solid.
- I caught ONE real issue → CONDITIONAL PASS (not rubber-stamp): the v0.8.1 positive E2E used a FAST-EXIT fixture, which F-010 now DEFINES as a failed host — it passed only by racing the 5s grace (kill brain first) = hollow + flake-prone on slow kitsubito AND left the RESIDENT path with zero E2E coverage. Required todlando swap in a long-lived resident fixture + assert resident-past-grace + phe None. Resolved 2 other gater notes: dropped the unobtainable `&lt;code&gt;` from the REQ title (detached spawn handle dropped → exit code unreachable; reaping it crosses the daemon-authoritative-via-perch seam, correctly rejected), and accepted clear-on-Resident.
- todlando RE-SPUN @0aad7b9: reworked livehost_bootrace_e2e (fixture `psychebin ready &lt;throwaway&gt;` = long-lived blocking listener, bare-token install-dir-resolved for REQ-INSTALL-11 fidelity; waits ~13s past grace+tick → is_process_alive → asserts {id}-psyche pid STILL ALIVE Resident AND parent phe None = the missing Resident-path proof). Reason reword applied 3 spots. todlando reports full gate green local (clippy -D warnings, traceable EXIT0, both real-daemon E2Es, units incl residency_classification, xtask).

IMMEDIATE NEXT STEP (resume here): RE-GATE the re-spin @0aad7b9 — (1) code-read the reworked livehost_bootrace_e2e to confirm it genuinely waits past PSYCHE_RESIDENCY_GRACE + one reconcile tick, probes is_process_alive, and asserts Resident + phe None (timing-robust, not racing); (2) verify CI GREEN BOTH runners on 0aad7b9 (watch for the kitsubito &quot;Build notify-shell&quot; crates.io curl flake → `gh run rerun --failed` clears it, NOT code). On green → hand deployah the v0.8.2 RELEASE GO (counter 18, PATCH, vetted version-framed changelog) → on publish + hashes, PING PERRI for re-validation.

perri's re-validation plan (recorded, she's not blocked): on my v0.8.2 ping she reverts her greedy --prompt back to a STRICT single-value parser (so spt-core's tokenize-then-fill is what's under test, not her workaround), re-runs ci/psyche/live-relay-int.sh psyche leg, confirms tokenize-then-fill hosts + psyche_host_error stamps on a forced fast-exit. She's meanwhile doing her v0.2.0 per-OS re-release + cplugs v0.1.2.

Roles holding: todlando=executor (built + re-spun, self-drives, watching CI) · deployah=publisher (11 clean legs, ships counter 18 on my GO) · perri=adapter builder/validator (needs replies to continue). MINOR open: peer-pump STALL ~7-8min after daemon start (perri-reported, non-blocking) — flagged todlando to investigate while in brainproc, NOT scope-creeping v0.8.2.
&lt;/live-context&gt;
&lt;project-context&gt;
spt-core v0.8.2 IN-FLIGHT (re-gate pending) — two perri-dogfood follow-up defect fixes, branch v0.8.2-templating-residency @0aad7b9 (off main = v0.8.1, counter 17 PUBLIC). Both REQs minted registry-first + activated [impl,unit,int]:

- REQ-HAZARD-TEMPLATE-ARGV-FILL (F-009): command-template substitution must fill argv ELEMENTS, not a re-tokenized string. Was fill_template(into string)→tokenize(split) so a multi-word/quote/`;` {key} value split/injected (perri's {psyche_prompt} → argv[6..12]). FIX shipped on branch: fill_template_tokens = tokenize TEMPLATE first, then fill each token → value is always exactly one argv element. Applies to every [session.&lt;role&gt;] template (psyche_init/extractor/notif); digest dodged it only via single-token fills.
- REQ-HAZARD-LIVEHOST-NONRESIDENT (F-010): a daemon-hosted Psyche that spawns then EXITS IMMEDIATELY is a host failure. The v0.8.1 psyche_host_error stamped only on spawn() Err; a detached spawn-Ok-then-die left a phantom nested {id}-psyche perch online with a dead pid + clean parent. FIX on branch: reconcile-tick residency-confirm (DIRECT is_process_alive pid-probe past a 5s grace) → Failed stamps parent psyche_host_error{reason:&quot;host not resident within 5s (psyche perch missing/dead pid)&quot;} + de-onlines the phantom + un-hosts + arms a 5s cooldown (no spawn-storm); the stamp CLEAR moved from spawn-Ok to the confirmed-Resident arm (a spawn-Ok is not a host success). Sibling of REQ-HAZARD-LIVEHOST-BOOT-RACE (v0.8.1).

These build directly on v0.8.1 (counter 17): the harness-reachable psyche_host_error signal on the Self perch info.json, surfaced by `spt endpoint list`/`whoami`. v0.8.0=16, v0.8.1=17, v0.8.2→18.

END GOAL (unchanged): spt-claude-code (perri's repo) is the v1 ACCEPTANCE PROOF that spt-core is a harness-independent agent-ecosystem core, proving parity with legacy claude_skill_owl (which retires once parity holds). perri's dogfood IS the parity validation: v0.8.1 unblocked her live-agent-Psyche proof (REQ-INSTALL-11 + F-006 proven); v0.8.2 closes the two residual defects her instrumented run surfaced (F-009 templating, F-010 residency-masking). The loop is healthy: perri finds gap (runtime-grounded) → doyle designs/mints REQ → todlando builds → doyle gates → deployah ships → perri re-validates.
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>


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:?}",
    );
}


Echo commune = FYI (my Psyche), no ACK.

**Code-read PASS** on re-spun `livehost_bootrace_e2e.rs`. Verified all 3 gate criteria:
- **Waits past grace+tick**: line 339-342, 13s busy-wait *after* hosted-detection (grace ~5s + reconcile tick ~5s = ~10s min; 13s clears it, anchored after host so even safer — not racing).
- **Probes `is_process_alive`**: line 343-344, `read_pid` → `is_process_alive` = the SAME probe the impl uses (not masking `is_perch_alive`).
- **Asserts Resident + phe None**: line 393-405, `psyche_alive` AND `phe.is_none()`. Long-lived `psychebin ready <throwaway>` fixture (blocking recv loop), bare-token install-dir resolved (REQ-INSTALL-11 fidelity, sanity line 179-189).

Matches todlando's claimed rework exactly. Now gate #2: CI green both runners. Currently IN_PROGRESS. Watch the run to completion.


Bash

gh run watch 27658258329 --exit-status > "$TMPDIR/ci-watch.log" 2>&1; echo "WATCH_EXIT=$?"; gh run view 27658258329 --json status,conclusion,jobs -q '.status,.conclusion,([.jobs[]|{name,status,conclusion}])' 2>&1

Watch CI run 27658258329 to completion


Command running in background with ID: brmv2mlb1. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\brmv2mlb1.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>brmv2mlb1</task-id>
<tool-use-id>toolu_015qzGwE2KcRgJStVxzsMy8b</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\brmv2mlb1.output</output-file>
<status>completed</status>
<summary>Background command "Watch CI run 27658258329 to completion" completed (exit code 0)</summary>
</task-notification>


Watching CI run 27658258329 (background, harness notifies on completion).

**Re-gate status @0aad7b9:**
- ✅ Gate 1 (code-read re-spun E2E): **PASS** — resident-path proof genuine, timing-robust.
- ⏳ Gate 2 (CI green both runners): watching. kitsubito Linux + hfenduleam Windows tests in progress; n1-gate Linux already PASS.

On CI green → hand deployah v0.8.2 RELEASE GO (counter 18, PATCH). If kitsubito hits the known `Build notify-shell` crates.io curl flake → `gh run rerun --failed`, not code. Waiting on CI.


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


1	/usr/bin/bash: line 4: /ci-watch.log: Permission denied
2	WATCH_EXIT=1
3	queued
4
5	[{"conclusion":"","name":"test (self-hosted, Windows, hfenduleam)","status":"in_progress"},{"conclusion":"success","name":"n1-gate (self-hosted, Linux, kitsubito)","status":"completed"},{"conclusion":"","name":"traceability","status":"queued"},{"conclusion":"","name":"test (self-hosted, Linux, kitsubito)","status":"in_progress"},{"conclusion":"","name":"n1-gate (self-hosted, Windows, hfenduleam)","status":"queued"}]
6

/usr/bin/bash: line 4: /ci-watch.log: Permission denied
WATCH_EXIT=1
queued

[{"conclusion":"","name":"test (self-hosted, Windows, hfenduleam)","status":"in_progress"},{"conclusion":"success","name":"n1-gate (self-hosted, Linux, kitsubito)","status":"completed"},{"conclusion":"","name":"traceability","status":"queued"},{"conclusion":"","name":"test (self-hosted, Linux, kitsubito)","status":"in_progress"},{"conclusion":"","name":"n1-gate (self-hosted, Windows, hfenduleam)","status":"queued"}]


Watch botched — `$TMPDIR` empty → wrote to `/ci-watch.log` (denied), `gh run watch` exited early. CI still in progress. Redo with proper log path.


gh run watch 27658258329 --exit-status; echo "WATCH_EXIT=$?"; gh run view 27658258329 --json status,conclusion,jobs -q '([.jobs[]|{name,status,conclusion}])'

Watch CI run to completion (proper)


Command running in background with ID: b0fkc5rko. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\b0fkc5rko.output. You will be notified when it completes. To check interim output, use Read on that file path.


CI watch re-armed (background). Waiting for both runners to finish.


<task-notification>
<task-id>b0fkc5rko</task-id>
<tool-use-id>toolu_01RkjhPETRhNhhVZGokt94Jm</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\b0fkc5rko.output</output-file>
<status>completed</status>
<summary>Background command "Watch CI run to completion (proper)" completed (exit code 0)</summary>
</task-notification>