1	//! `mock-session` — the generic mock adapter's "agent session". A trivial,
2	//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
3	//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
4	//! `[session.self]` template, it does what a real session's boot hook would do:
5	//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
6	//!
7	//! It shells out to the `spt` binary exactly as a real adapter would — resolved
8	//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
9	//! the inbound contract is harness-agnostic: a dumb helper drives it with only
10	//! `spt api` calls.
11
12	use std::process::Command;
13
14	fn arg(args: &[String], flag: &str) -> Option<String> {
15	    args.iter()
16	        .position(|a| a == flag)
17	        .and_then(|i| args.get(i + 1).cloned())
18	}
19
20	fn main() {
21	    let args: Vec<String> = std::env::args().skip(1).collect();
22
23	    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
24	    // binds its nested perch; the child just needs to launch). `echo` is the
25	    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
26	    match arg(&args, "--mode").as_deref() {
27	        Some("psyche") => {
28	            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
29	            println!("MOCK_PSYCHE_UP:{id}");
30	            return;
31	        }
32	        Some("echo") => {
33	            use std::io::Read;
34	            let mut history = String::new();
35	            let _ = std::io::stdin().read_to_string(&mut history);
36	            // A deterministic brief; a real echo summarizes the history it read.
37	            println!("mock echo brief ({} history bytes)", history.len());
38	            return;
39	        }
40	        Some("dummy") => {
41	            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
42	            // stand-in for a real harness session. Binds its perch (the harness
43	            // contract) exactly like the default stand-in, then prints a stdout
44	            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
45	            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
46	            // attach path can be exercised end-to-end (the default mode exits
47	            // immediately, which is why the v0.12.0 mock tests never caught the
48	            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
49	            // endpoint died" from "a real harness failed to launch."
50	            use std::io::Write;
51	            let id = arg(&args, "--id").unwrap_or_else(|| {
52	                eprintln!("mock-session --mode dummy: --id required");
53	                std::process::exit(2);
54	            });
55	            let session_id =
56	                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
57	            let spt = arg(&args, "--spt")
58	                .or_else(|| std::env::var("SPT_BIN").ok())
59	                .unwrap_or_else(|| "spt".to_string());
60	            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
61	            let endpoint_type = arg(&args, "--type");
62	            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
63	                .and_then(|s| s.parse().ok())
64	                .unwrap_or(500);
65
66	            // Bind the perch live (spt-hosted direct bind, mirroring the default
67	            // stand-in below). A failure is fatal — the fixture must come up.
68	            let mut bind_args = vec![
69	                "api".to_string(),
70	                "--adapter".to_string(),
71	                adapter,
72	                "bind".to_string(),
73	                id.clone(),
74	                "--set-session-id".to_string(),
75	                session_id,
76	            ];
77	            if let Some(ty) = &endpoint_type {
78	                bind_args.push("--type".to_string());
79	                bind_args.push(ty.clone());
80	            }
81	            let bind = Command::new(&spt).args(&bind_args).status();
82	            if !matches!(bind, Ok(s) if s.success()) {
83	                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
84	                std::process::exit(1);
85	            }
86
87	            println!("DUMMY_HARNESS_READY:{id}");
88	            let _ = std::io::stdout().flush();
89	            // Heartbeat forever — only an external kill (tab-close job reap, daemon
90	            // stop, or an explicit kill) ends it.
91	            let mut n: u64 = 0;
92	            loop {
93	                println!("DUMMY_HARNESS_TICK:{id}:{n}");
94	                let _ = std::io::stdout().flush();
95	                n += 1;
96	                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
97	            }
98	        }
99	        _ => {} // default: the agent-session stand-in below.
100	    }
101
102	    let id = arg(&args, "--id").unwrap_or_else(|| {
103	        eprintln!("mock-session: --id required");
104	        std::process::exit(2);
105	    });
106	    let session_id = arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
107	    let spt = arg(&args, "--spt")
108	        .or_else(|| std::env::var("SPT_BIN").ok())
109	        .unwrap_or_else(|| "spt".to_string());
110	    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.
111	    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.
112	    let endpoint_type = arg(&args, "--type");
113	    let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
114
115	    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.
116	    let mut bind_args = vec![
117	        "api".to_string(),
118	        "--adapter".to_string(),
119	        adapter.clone(),
120	        "bind".to_string(),
121	        id.clone(),
122	        "--set-session-id".to_string(),
123	        session_id.clone(),
124	    ];
125	    if let Some(ty) = &endpoint_type {
126	        bind_args.push("--type".to_string());
127	        bind_args.push(ty.clone());
128	    }
129	    let bind = Command::new(&spt).args(&bind_args).status();
130	    if !matches!(bind, Ok(s) if s.success()) {
131	        eprintln!("mock-session: api bind failed: {bind:?}");
132	        std::process::exit(1);
133	    }
134
135	    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.
136	    let state = Command::new(&spt)
137	        .args([
138	            "api",
139	            "--adapter",
140	            &adapter,
141	            "state",
142	            "idle",
143	            &id,
144	            "--session-id",
145	            &session_id,
146	        ])
147	        .status();
148	    if !matches!(state, Ok(s) if s.success()) {
149	        eprintln!("mock-session: api state idle failed: {state:?}");
150	        std::process::exit(1);
151	    }
152
153	    println!("MOCK_SESSION_READY:{id}");
154	}
155

//! `mock-session` — the generic mock adapter's "agent session". A trivial,
//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
//! `[session.self]` template, it does what a real session's boot hook would do:
//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
//!
//! It shells out to the `spt` binary exactly as a real adapter would — resolved
//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
//! the inbound contract is harness-agnostic: a dumb helper drives it with only
//! `spt api` calls.

use std::process::Command;

fn arg(args: &[String], flag: &str) -> Option<String> {
    args.iter()
        .position(|a| a == flag)
        .and_then(|i| args.get(i + 1).cloned())
}

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();

    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
    // binds its nested perch; the child just needs to launch). `echo` is the
    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
    match arg(&args, "--mode").as_deref() {
        Some("psyche") => {
            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
            println!("MOCK_PSYCHE_UP:{id}");
            return;
        }
        Some("echo") => {
            use std::io::Read;
            let mut history = String::new();
            let _ = std::io::stdin().read_to_string(&mut history);
            // A deterministic brief; a real echo summarizes the history it read.
            println!("mock echo brief ({} history bytes)", history.len());
            return;
        }
        Some("dummy") => {
            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
            // stand-in for a real harness session. Binds its perch (the harness
            // contract) exactly like the default stand-in, then prints a stdout
            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
            // attach path can be exercised end-to-end (the default mode exits
            // immediately, which is why the v0.12.0 mock tests never caught the
            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
            // endpoint died" from "a real harness failed to launch."
            use std::io::Write;
            let id = arg(&args, "--id").unwrap_or_else(|| {
                eprintln!("mock-session --mode dummy: --id required");
                std::process::exit(2);
            });
            let session_id =
                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
            let spt = arg(&args, "--spt")
                .or_else(|| std::env::var("SPT_BIN").ok())
                .unwrap_or_else(|| "spt".to_string());
            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
            let endpoint_type = arg(&args, "--type");
            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
                .and_then(|s| s.parse().ok())
                .unwrap_or(500);

            // Bind the perch live (spt-hosted direct bind, mirroring the default
            // stand-in below). A failure is fatal — the fixture must come up.
            let mut bind_args = vec![
                "api".to_string(),
                "--adapter".to_string(),
                adapter,
                "bind".to_string(),
                id.clone(),
                "--set-session-id".to_string(),
                session_id,
            ];
            if let Some(ty) = &endpoint_type {
                bind_args.push("--type".to_string());
                bind_args.push(ty.clone());
            }
            let bind = Command::new(&spt).args(&bind_args).status();
            if !matches!(bind, Ok(s) if s.success()) {
                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
                std::process::exit(1);
            }

            println!("DUMMY_HARNESS_READY:{id}");
            let _ = std::io::stdout().flush();
            // Heartbeat forever — only an external kill (tab-close job reap, daemon
            // stop, or an explicit kill) ends it.
            let mut n: u64 = 0;
            loop {
                println!("DUMMY_HARNESS_TICK:{id}:{n}");
                let _ = std::io::stdout().flush();
                n += 1;
                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
            }
        }
        _ => {} // default: the agent-session stand-in below.
    }

    let id = arg(&args, "--id").unwrap_or_else(|| {
        eprintln!("mock-session: --id required");
        std::process::exit(2);
    });
    let session_id = arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
    let spt = arg(&args, "--spt")
        .or_else(|| std::env::var("SPT_BIN").ok())
        .unwrap_or_else(|| "spt".to_string());
    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.
    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.
    let endpoint_type = arg(&args, "--type");
    let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());

    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.
    let mut bind_args = vec![
        "api".to_string(),
        "--adapter".to_string(),
        adapter.clone(),
        "bind".to_string(),
        id.clone(),
        "--set-session-id".to_string(),
        session_id.clone(),
    ];
    if let Some(ty) = &endpoint_type {
        bind_args.push("--type".to_string());
        bind_args.push(ty.clone());
    }
    let bind = Command::new(&spt).args(&bind_args).status();
    if !matches!(bind, Ok(s) if s.success()) {
        eprintln!("mock-session: api bind failed: {bind:?}");
        std::process::exit(1);
    }

    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.
    let state = Command::new(&spt)
        .args([
            "api",
            "--adapter",
            &adapter,
            "state",
            "idle",
            &id,
            "--session-id",
            &session_id,
        ])
        .status();
    if !matches!(state, Ok(s) if s.success()) {
        eprintln!("mock-session: api state idle failed: {state:?}");
        std::process::exit(1);
    }

    println!("MOCK_SESSION_READY:{id}");
}


1	//! Wave 1 (v0.12.1) — the DUMMY-HARNESS fixture smoke test. The permanent
2	//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the
3	//! REAL spt-hosted bringup path end-to-end —
4	//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach
5	//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and
6	//! NOT a mock that exits immediately.
7	//!
8	//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the
9	//! harness contract), prints a stdout heartbeat on an interval, and stays alive
10	//! until killed — so "broker wedged / endpoint died" is cleanly separable from "a
11	//! real harness failed to launch." Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,
12	//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.
13	//!
14	//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the
15	//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the
16	//! live heartbeat output. Bounded; all spawned pids reaped scoped (never
17	//! machine-wide — shared runner).
18
19	use std::io::Read;
20	use std::path::{Path, PathBuf};
21	use std::process::{Child, Command, Output, Stdio};
22	use std::time::{Duration, Instant};
23
24	mod common;
25	use common::CommandNoWindowExt;
26
27	use spt_store::perch::{self, ParentHint};
28
29	fn kill_pid(pid: u32) {
30	    #[cfg(windows)]
31	    let _ = Command::new("taskkill")
32	        .no_window()
33	        .args(["/PID", &pid.to_string(), "/F", "/T"])
34	        .output();
35	    #[cfg(unix)]
36	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
37	}
38
39	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
40	    let (tx, rx) = std::sync::mpsc::channel();
41	    std::thread::spawn(move || {
42	        let _ = tx.send(cmd.output());
43	    });
44	    rx.recv_timeout(deadline)
45	        .expect("captured spt call must complete within its deadline")
46	        .expect("run spt")
47	}
48
49	fn ready_pid(path: &Path) -> Option<u32> {
50	    let s = std::fs::read_to_string(path).ok()?;
51	    serde_json::from_str::<serde_json::Value>(&s)
52	        .ok()?
53	        .get("pid")?
54	        .as_u64()
55	        .map(|p| p as u32)
56	}
57
58	fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
59	    let deadline = Instant::now() + budget;
60	    while Instant::now() < deadline {
61	        if let Some(p) = ready_pid(path) {
62	            return Some(p);
63	        }
64	        std::thread::sleep(Duration::from_millis(50));
65	    }
66	    None
67	}
68
69	fn sibling_bin(name: &str) -> PathBuf {
70	    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
71	        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
72	}
73
74	#[test]
75	fn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {
76	    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node
77	    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces
78	    //    as net-stream events, so it requires a net-bound broker — a net-less broker
79	    //    refuses the loopback dial ("net disabled"). Unlike the livehost/reconcile
80	    //    tests (which only inspect perch state), the rc-attach path needs net up. ──
81	    let home = tempfile::tempdir().unwrap();
82	    std::env::set_var("SPT_HOME", home.path());
83
84	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
85	    let exe_suffix = spt_bin
86	        .extension()
87	        .map(|e| format!(".{}", e.to_string_lossy()))
88	        .unwrap_or_default();
89	    let mock_session = sibling_bin("mock-session");
90	    assert!(
91	        mock_session.exists(),
92	        "the dummy-harness program must be built: {} \
93	         (pre-build: cargo build -p mock-adapter --bin mock-session)",
94	        mock_session.display()
95	    );
96
97	    // ── (2) Register the DUMMY-HARNESS adapter: a kind=harness adapter whose
98	    //    [session.self] runs the long-lived dummy. The broker fills {id}/{session_id};
99	    //    the program token is the absolute mock-session path. The dummy shells the
100	    //    same `spt` to bind (via --spt). ──
101	    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
102	    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
103	    let self_cmd = format!(
104	        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
105	         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
106	    );
107	    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only
108	    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —
109	    // the online signal is gated to the reconcile host predicate). A LONG-LIVED psyche
110	    // (a copy of `spt` run as `ready {id}`, mirroring brain_restart_psyche_dup) so the
111	    // brain's reconcile hosts a resident `{id}-psyche` — the same shape the Wave 2
112	    // VIEWER-CLOSE / psyche-reap gates need.
113	    let src = perch::spt_home().join("srcs").join("dummyharness");
114	    std::fs::create_dir_all(&src).unwrap();
115	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
116	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
117	    let drops = home.path().join("drops");
118	    std::fs::create_dir_all(&drops).unwrap();
119	    let dir = drops.to_string_lossy().replace('\\', "/");
120	    let manifest_toml = format!(
121	        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
122	         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
123	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
124	         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
125	    );
126	    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
127	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
128
129	    // ── (3) The REAL daemon broker process. ──
130	    let brain_log = home.path().join("brain.stderr.log");
131	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
132	    let mut broker: Child = Command::new(&spt_bin)
133	        .no_window()
134	        .args(["daemon", "run"])
135	        .env("SPT_HOME", home.path())
136	        .stdout(Stdio::null())
137	        .stderr(Stdio::from(brain_log_file))
138	        .spawn()
139	        .expect("spawn spt daemon run (broker process)");
140	    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
141	    {
142	        Some(p) => p,
143	        None => {
144	            let _ = broker.kill();
145	            let _ = broker.wait();
146	            panic!(
147	                "PRECONDITION: brain never came up.\n{}",
148	                std::fs::read_to_string(&brain_log).unwrap_or_default()
149	            );
150	        }
151	    };
152
153	    // ── (4) The REAL spt-hosted bringup: endpoint run --start (broker spawns the
154	    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──
155	    let id = "dummy7";
156	    let run = {
157	        let mut cmd = Command::new(&spt_bin);
158	        cmd.no_window()
159	            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
160	            .env("SPT_HOME", home.path());
161	        output_bounded(cmd, Duration::from_secs(45))
162	    };
163
164	    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
165	    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
166
167	    // The dummy binds its perch live within a few seconds of the broker PTY spawn.
168	    let mut online = false;
169	    let deadline = Instant::now() + Duration::from_secs(20);
170	    while Instant::now() < deadline {
171	        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
172	            online = true;
173	            break;
174	        }
175	        std::thread::sleep(Duration::from_millis(150));
176	    }
177
178	    // The broker-spawned harness pid (from the terse `ENDPOINT_RUN:… pid=NNN`
179	    // machine token) — the long-lived PTY child itself, NOT the transient `spt api
180	    // bind` subprocess that stamps info.json's pid. It must stay alive (heartbeating).
181	    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
182	    let harness_pid: Option<u32> = run_stderr
183	        .split("pid=")
184	        .nth(1)
185	        .and_then(|s| s.split_whitespace().next())
186	        .and_then(|s| s.trim().parse().ok());
187	    let alive_after = {
188	        std::thread::sleep(Duration::from_millis(700));
189	        harness_pid.map(spt_store::proc::is_process_alive).unwrap_or(false)
190	    };
191
192	    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──
193	    let rc_err = home.path().join("rc.stderr.log");
194	    let rc_err_file = std::fs::File::create(&rc_err).expect("create rc stderr log");
195	    let mut rc: Child = Command::new(&spt_bin)
196	        .no_window()
197	        .args(["rc", id])
198	        .env("SPT_HOME", home.path())
199	        .stdout(Stdio::piped())
200	        .stderr(Stdio::from(rc_err_file))
201	        .stdin(Stdio::piped()) // keep stdin OPEN — a closed stdin makes the rc pump EOF-detach before streaming
202	        .spawn()
203	        .expect("spawn spt rc <id>");
204	    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window
205	    let mut rc_out = rc.stdout.take().unwrap();
206	    let (tx, rx) = std::sync::mpsc::channel::<String>();
207	    std::thread::spawn(move || {
208	        let mut buf = [0u8; 4096];
209	        let mut acc = String::new();
210	        loop {
211	            match rc_out.read(&mut buf) {
212	                Ok(0) => break,
213	                Ok(n) => {
214	                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));
215	                    if acc.contains("DUMMY_HARNESS_TICK") {
216	                        let _ = tx.send(acc.clone());
217	                        break;
218	                    }
219	                }
220	                Err(_) => break,
221	            }
222	        }
223	        let _ = tx.send(acc);
224	    });
225	    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();
226	    let rc_saw_tick = rc_captured.contains("DUMMY_HARNESS_TICK");
227
228	    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
229	    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();
230	    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did
231	    // NOT RC_FAIL (no "no live session" / loopback-dial refusal).
232	    let rc_connected = rc_stderr.contains("PUMP_IPC_READER") && !rc_stderr.contains("RC_FAIL");
233	    eprintln!(
234	        "=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \
235	         alive_after={alive_after} rc_connected={rc_connected} rc_saw_tick={rc_saw_tick} \
236	         rc_stdout_bytes={} ===\n=== endpoint run stderr ===\n{}\n=== rc stderr ===\n{rc_stderr}\
237	         \n=== rc stdout (first 400) ===\n{}",
238	        run.status.success(),
239	        rc_captured.len(),
240	        String::from_utf8_lossy(&run.stderr),
241	        rc_captured.chars().take(400).collect::<String>(),
242	    );
243
244	    // ── (6) Reap scoped: kill rc, the harness, the brain, daemon-stop, broker. ──
245	    kill_pid(rc.id());
246	    let _ = rc.kill();
247	    let _ = rc.wait();
248	    if let Some(p) = harness_pid {
249	        kill_pid(p);
250	    }
251	    // The reconcile may have hosted a `{id}-psyche` — reap it too (scoped).
252	    let psyche_perch =
253	        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
254	    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
255	        kill_pid(p);
256	    }
257	    let _ = {
258	        let mut cmd = Command::new(&spt_bin);
259	        cmd.no_window().args(["daemon", "stop"]).env("SPT_HOME", home.path());
260	        output_bounded(cmd, Duration::from_secs(20))
261	    };
262	    kill_pid(brain_pid);
263	    let _ = broker.kill();
264	    let _ = broker.wait();
265
266	    assert!(
267	        run.status.success(),
268	        "endpoint run --start must succeed: {}",
269	        String::from_utf8_lossy(&run.stderr)
270	    );
271	    assert!(
272	        online,
273	        "the dummy harness must bind its perch ONLINE via the real broker PTY bringup.\n\
274	         === brain stderr ===\n{brain_stderr}"
275	    );
276	    assert!(
277	        alive_after,
278	        "the dummy harness PTY child must STAY ALIVE (heartbeating), not exit like the \
279	         default mock — harness_pid={harness_pid:?}"
280	    );
281	    // `rc_connected` (PUMP_IPC_READER present, no RC_FAIL) was the Wave-1 proxy for
282	    // "the attach wired up." It is now SUBSUMED by the `rc_saw_tick` keystone gate
283	    // below: receiving the harness's live output is a strictly stronger proof of a
284	    // wired attach than the setup log line (and the log line races the now-fast
285	    // teardown — rc delivers a tick and exits before its stderr file flushes). Kept
286	    // informational in the DIAGNOSTIC above; the hard proof is output delivery.
287	    let _ = rc_connected;
288	    // KEYSTONE GATE (v0.12.1 Wave 2 L0): a clean `spt rc` attach to this LIVE,
289	    // ALREADY-PRODUCING (non-empty OutputLog ring) endpoint-run harness DELIVERS its
290	    // `[session.self]` stdout — the operator's "attach shows no output" is fixed. This
291	    // is the exact case the v0.12.0 mock suite + the loopback E2Es lacked (they only
292	    // ever attach to an idle/just-spawned child, so `become_controller` replays an
293	    // empty ring and never triggers the replay-vs-forward IPC deadlock). The fix
294	    // (serve_attach forwards on a SEPARATE broker conn from the one it receives the
295	    // replay on) is what makes this pass.
296	    // [int->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
297	    assert!(
298	        rc_saw_tick,
299	        "`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \
300	         output (got {} bytes) — the broker must forward an already-producing harness's \
301	         PTY to the attach without the replay-vs-forward deadlock.\n=== rc stderr ===\n{rc_stderr}\
302	         \n=== brain stderr ===\n{brain_stderr}",
303	        rc_captured.len()
304	    );
305	}
306
307	// ── REPRO-FIRST regression for REQ-HAZARD-RC-ATTACH-ONLINE-RACE ──────────────
308	// THE BUG (doyle root-caused): `spt endpoint run` in an ATTACH terminal action
309	// (no `--start`) attaches BEFORE the freshly-spawned endpoint is online. In
310	// `cmd_endpoint_run` the flow is `launch_harness_brokered_in` (returns once the
311	// harness PROCESS is spawned) → `run_attach`, with NO await-online between them.
312	// The broker-PTY bind (info status→STATUS_ONLINE + a live session) lands ASYNC,
313	// so the immediate `run_attach` loses the race: it either short-circuits on the
314	// offline status-gate ("offline — nothing to attach", REQ-HAZARD-RC-ATTACH-FAILFAST
315	// in rc.rs run_attach) or hits "no live session for endpoint" (printed as
316	// RC_FAIL) — never connecting to the live PTY, so NO heartbeat is ever seen.
317	//
318	// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live
319	// DUMMY_HARNESS_TICK — proof the attach waited for bringup and connected to the
320	// live session — exactly as the --start + separate `rc` path above does.
321	//
322	// Pre-fix this test is RED: the captured output shows the offline/no-live-session
323	// bail and NO heartbeat. (The fix — await-online between launch and run_attach —
324	// lands in a later step; this commit touches ONLY this test file.)
325	//
326	// Same isolated-SPT_HOME + dummyharness fixture + scoped full-tree reap as the
327	// sister test above; the ONLY behavioural difference is dropping `--start` so the
328	// SAME process runs both bringup AND attach (the bug surface), captured bounded.
329	// [int->REQ-HAZARD-RC-ATTACH-ONLINE-RACE]
330	#[test]
331	fn endpoint_run_attach_awaits_online_before_attaching() {
332	    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attach rides a broker loopback
333	    //    conn that needs a net-bound broker — identical to the sister test). ──
334	    let home = tempfile::tempdir().unwrap();
335	    std::env::set_var("SPT_HOME", home.path());
336
337	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
338	    let exe_suffix = spt_bin
339	        .extension()
340	        .map(|e| format!(".{}", e.to_string_lossy()))
341	        .unwrap_or_default();
342	    let mock_session = sibling_bin("mock-session");
343	    if !mock_session.exists() {
344	        panic!(
345	            "the dummy-harness program must be built: {} \
346	             (pre-build: cargo build -p mock-adapter --bin mock-session)",
347	            mock_session.display()
348	        );
349	    }
350
351	    // ── (2) Register the DUMMY-HARNESS adapter (verbatim shape from the sister
352	    //    test: long-lived kind=harness, heartbeats, binds its own perch; a
353	    //    resident psyche so the brain's reconcile marks status=online). ──
354	    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
355	    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
356	    let self_cmd = format!(
357	        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
358	         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
359	    );
360	    let src = perch::spt_home().join("srcs").join("dummyharness");
361	    std::fs::create_dir_all(&src).unwrap();
362	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
363	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
364	    let drops = home.path().join("drops");
365	    std::fs::create_dir_all(&drops).unwrap();
366	    let dir = drops.to_string_lossy().replace('\\', "/");
367	    let manifest_toml = format!(
368	        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
369	         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
370	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
371	         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
372	    );
373	    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
374	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
375
376	    // ── (3) The REAL daemon broker process. ──
377	    let brain_log = home.path().join("brain.stderr.log");
378	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
379	    let mut broker: Child = Command::new(&spt_bin)
380	        .no_window()
381	        .args(["daemon", "run"])
382	        .env("SPT_HOME", home.path())
383	        .stdout(Stdio::null())
384	        .stderr(Stdio::from(brain_log_file))
385	        .spawn()
386	        .expect("spawn spt daemon run (broker process)");
387	    let brain_pid =
388	        match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30)) {
389	            Some(p) => p,
390	            None => {
391	                let _ = broker.kill();
392	                let _ = broker.wait();
393	                panic!(
394	                    "PRECONDITION: brain never came up.\n{}",
395	                    std::fs::read_to_string(&brain_log).unwrap_or_default()
396	                );
397	            }
398	        };
399
400	    let id = "dummyrace1";
401	    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
402	    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
403
404	    // ── (4) DETERMINISTIC OFFLINE PRECONDITION. The bug only surfaces reliably when
405	    //    the SAME id is already KNOWN-OFFLINE at the moment of the attach-by-default
406	    //    run, so `run_attach`'s status-gate short-circuits ("offline — nothing to
407	    //    attach") pre-fix instead of racing a fresh bind (a fresh id can win the race
408	    //    far enough to connect-and-tick — a FALSE green). So: (4a) bring the endpoint
409	    //    up cleanly with --start, (4b) wait ONLINE, (4c) kill the harness, (4d) poll
410	    //    until the broker's liveness reconcile (reconcile_hosted_liveness, livehost.rs)
411	    //    stamps the perch OFFLINE — that dead-but-offline endpoint is the precondition
412	    //    that makes the next attach deterministically hit the offline gate pre-fix. ──
413
414	    // (4a) Clean --start bringup (captured bounded; this run returns once the harness
415	    //      process is spawned — its `pid=` token names the FIRST harness incarnation).
416	    let start_run = {
417	        let mut cmd = Command::new(&spt_bin);
418	        cmd.no_window()
419	            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
420	            .env("SPT_HOME", home.path());
421	        output_bounded(cmd, Duration::from_secs(45))
422	    };
423	    let start_stderr = String::from_utf8_lossy(&start_run.stderr).to_string();
424	    let harness_pid_1: Option<u32> = start_stderr
425	        .split("pid=")
426	        .nth(1)
427	        .and_then(|s| s.split_whitespace().next())
428	        .and_then(|s| s.trim().parse().ok());
429
430	    // (4b) Wait for the dummy to bind its perch ONLINE via the real broker PTY.
431	    let mut online_1 = false;
432	    let deadline = Instant::now() + Duration::from_secs(20);
433	    while Instant::now() < deadline {
434	        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
435	            online_1 = true;
436	            break;
437	        }
438	        std::thread::sleep(Duration::from_millis(150));
439	    }
440
441	    // (4c) Drive the endpoint OFFLINE: kill the first harness incarnation. Its broker
442	    //      session then drains and the next reconcile sweep sees no live session for a
443	    //      dead, online-latched, controllable perch → marks it offline.
444	    if let Some(p) = harness_pid_1 {
445	        kill_pid(p);
446	    }
447
448	    // (4d) POLL info.status until STATUS_OFFLINE (bounded). This is the precondition:
449	    //      the attach-by-default run below will read this still-OFFLINE status pre-fix.
450	    let mut went_offline = false;
451	    let off_deadline = Instant::now() + Duration::from_secs(15);
452	    while Instant::now() < off_deadline {
453	        if status().as_deref() == Some(spt_store::liveness::STATUS_OFFLINE) {
454	            went_offline = true;
455	            break;
456	        }
457	        std::thread::sleep(Duration::from_millis(150));
458	    }
459
460	    // ── (5) THE BUG SURFACE: `endpoint run` with NO `--start`, SAME id = attach-by-
461	    //    default re-bringup. The SAME process re-spawns the harness AND then runs the
462	    //    attach pump. Pre-fix the immediate attach reads the still-OFFLINE status →
463	    //    "offline — nothing to attach" → NO tick. Post-fix (await_endpoint_online)
464	    //    waits for the re-bringup to bind ONLINE → attach → tick delivered.
465	    //
466	    //    Spawn as a Child with a HELD write-end of stdin (a closed stdin makes the
467	    //    attach pump's stdin-reader hit EOF and CLEAN-DETACH before the 200ms
468	    //    heartbeat — output_bounded/cmd.output() closes stdin, so it can NEVER observe
469	    //    a tick, in BOTH pre- and post-fix runs). Stream stdout in a thread until
470	    //    DUMMY_HARNESS_TICK (the heartbeat-delivery keystone), exactly as the sister
471	    //    test's `rc` attach does. ──
472	    let run_err = home.path().join("run.stderr.log");
473	    let run_err_file = std::fs::File::create(&run_err).expect("create endpoint-run stderr log");
474	    let mut run: Child = Command::new(&spt_bin)
475	        .no_window()
476	        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id])
477	        .env("SPT_HOME", home.path())
478	        .stdout(Stdio::piped())
479	        .stderr(Stdio::from(run_err_file))
480	        .stdin(Stdio::piped()) // HOLD stdin open — a closed stdin EOF-detaches before the tick
481	        .spawn()
482	        .expect("spawn endpoint run (no --start, attach-by-default)");
483	    let _run_stdin = run.stdin.take(); // hold the write end open for the attach window
484	    let mut run_out = run.stdout.take().unwrap();
485	    let (tx, rx) = std::sync::mpsc::channel::<String>();
486	    std::thread::spawn(move || {
487	        let mut buf = [0u8; 4096];
488	        let mut acc = String::new();
489	        loop {
490	            match run_out.read(&mut buf) {
491	                Ok(0) => break,
492	                Ok(n) => {
493	                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));
494	                    if acc.contains("DUMMY_HARNESS_TICK") {
495	                        let _ = tx.send(acc.clone());
496	                        break;
497	                    }
498	                }
499	                Err(_) => break,
500	            }
501	        }
502	        let _ = tx.send(acc);
503	    });
504	    let run_captured = rx.recv_timeout(Duration::from_secs(12)).unwrap_or_default();
505
506	    // THE CLAIM (compute pre-reap, assert post-reap): the attach awaited the re-
507	    // bringup coming ONLINE, connected to the live session, and delivered the
508	    // harness's live heartbeat — the SAME live-PTY marker the sister test asserts.
509	    let saw_tick = run_captured.contains("DUMMY_HARNESS_TICK");
510	    let run_stderr_txt = std::fs::read_to_string(&run_err).unwrap_or_default();
511	    // Diagnostic discriminators for the pre-fix race losses (info only).
512	    let bailed_offline = run_captured.contains("offline — nothing to attach")
513	        || run_stderr_txt.contains("offline — nothing to attach");
514	    let bailed_no_session =
515	        run_captured.contains("no live session") || run_stderr_txt.contains("no live session");
516
517	    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
518	    eprintln!(
519	        "=== DIAGNOSTIC (ONLINE-RACE): online_1={online_1} went_offline={went_offline} \
520	         saw_tick={saw_tick} bailed_offline={bailed_offline} bailed_no_session={bailed_no_session} \
521	         run_stdout_bytes={} ===\n=== endpoint run stdout (first 800) ===\n{}\n\
522	         === endpoint run stderr ===\n{run_stderr_txt}\n=== brain stderr ===\n{brain_stderr}",
523	        run_captured.len(),
524	        run_captured.chars().take(800).collect::<String>(),
525	    );
526
527	    // ── (6) Reap the ENTIRE tree SCOPED before asserting (never leave a stray spt.exe
528	    //    — a survivor locks target\debug\spt.exe → EPERM next checkout). The endpoint
529	    //    is brought up TWICE, so reap BOTH harness incarnations. Then the re-bringup
530	    //    attach process (`run`), the reconcile-hosted psyche, the brain, daemon-stop,
531	    //    broker — same order as the sister test. ──
532	    let harness_pid_2: Option<u32> = run_stderr_txt
533	        .split("pid=")
534	        .nth(1)
535	        .and_then(|s| s.split_whitespace().next())
536	        .and_then(|s| s.trim().parse().ok());
537	    kill_pid(run.id());
538	    let _ = run.kill();
539	    let _ = run.wait();
540	    if let Some(p) = harness_pid_1 {
541	        kill_pid(p);
542	    }
543	    if let Some(p) = harness_pid_2 {
544	        kill_pid(p);
545	    }
546	    let psyche_perch =
547	        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
548	    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
549	        kill_pid(p);
550	    }
551	    let _ = {
552	        let mut cmd = Command::new(&spt_bin);
553	        cmd.no_window()
554	            .args(["daemon", "stop"])
555	            .env("SPT_HOME", home.path());
556	        output_bounded(cmd, Duration::from_secs(20))
557	    };
558	    kill_pid(brain_pid);
559	    let _ = broker.kill();
560	    let _ = broker.wait();
561	    // Poll-verify the broker + both harness incarnations are reaped (scoped).
562	    let reap_deadline = Instant::now() + Duration::from_secs(10);
563	    while Instant::now() < reap_deadline {
564	        let brain_gone = !spt_store::proc::is_process_alive(brain_pid);
565	        let h1_gone = harness_pid_1
566	            .map(|p| !spt_store::proc::is_process_alive(p))
567	            .unwrap_or(true);
568	        let h2_gone = harness_pid_2
569	            .map(|p| !spt_store::proc::is_process_alive(p))
570	            .unwrap_or(true);
571	        if brain_gone && h1_gone && h2_gone {
572	            break;
573	        }
574	        std::thread::sleep(Duration::from_millis(100));
575	    }
576
577	    // Precondition sanity (only meaningful if the env actually drove the endpoint
578	    // offline; if not, the test would be vacuous — surface it loudly rather than fake).
579	    assert!(
580	        online_1,
581	        "PRECONDITION: the --start bringup must bind ONLINE before we drive it offline.\n\
582	         === brain stderr ===\n{brain_stderr}"
583	    );
584	    assert!(
585	        went_offline,
586	        "PRECONDITION: killing the harness must drive the perch OFFLINE (broker liveness \
587	         reconcile) so the attach-by-default run deterministically hits the offline gate \
588	         pre-fix.\n=== brain stderr ===\n{brain_stderr}"
589	    );
590
591	    // ── (7) THE GATE. Pre-fix: RED (attach read the still-OFFLINE status, bailed
592	    //    "offline — nothing to attach", no heartbeat). Post-fix (await_endpoint_online
593	    //    before run_attach): GREEN (the re-bringup binds ONLINE, the attach connects,
594	    //    the heartbeat is delivered). ──
595	    assert!(
596	        saw_tick,
597	        "`spt endpoint run` WITHOUT --start (attach-by-default) against a known-OFFLINE id \
598	         must await the re-spawned endpoint coming ONLINE before attaching, then DELIVER its \
599	         live DUMMY_HARNESS_TICK — instead the immediate attach read the still-OFFLINE status \
600	         and bailed (bailed_offline={bailed_offline} bailed_no_session={bailed_no_session}, \
601	         {} stdout bytes, no heartbeat).\n=== endpoint run stdout ===\n{run_captured}\
602	         \n=== endpoint run stderr ===\n{run_stderr_txt}\n=== brain stderr ===\n{brain_stderr}",
603	        run_captured.len(),
604	    );
605	}
606

//! Wave 1 (v0.12.1) — the DUMMY-HARNESS fixture smoke test. The permanent
//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the
//! REAL spt-hosted bringup path end-to-end —
//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach
//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and
//! NOT a mock that exits immediately.
//!
//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the
//! harness contract), prints a stdout heartbeat on an interval, and stays alive
//! until killed — so "broker wedged / endpoint died" is cleanly separable from "a
//! real harness failed to launch." Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,
//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.
//!
//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the
//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the
//! live heartbeat output. Bounded; all spawned pids reaped scoped (never
//! machine-wide — shared runner).

use std::io::Read;
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};

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

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")
}

fn ready_pid(path: &Path) -> Option<u32> {
    let s = std::fs::read_to_string(path).ok()?;
    serde_json::from_str::<serde_json::Value>(&s)
        .ok()?
        .get("pid")?
        .as_u64()
        .map(|p| p as u32)
}

fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Some(p) = ready_pid(path) {
            return Some(p);
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

fn sibling_bin(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

#[test]
fn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {
    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node
    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces
    //    as net-stream events, so it requires a net-bound broker — a net-less broker
    //    refuses the loopback dial ("net disabled"). Unlike the livehost/reconcile
    //    tests (which only inspect perch state), the rc-attach path needs net up. ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    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();
    let mock_session = sibling_bin("mock-session");
    assert!(
        mock_session.exists(),
        "the dummy-harness program must be built: {} \
         (pre-build: cargo build -p mock-adapter --bin mock-session)",
        mock_session.display()
    );

    // ── (2) Register the DUMMY-HARNESS adapter: a kind=harness adapter whose
    //    [session.self] runs the long-lived dummy. The broker fills {id}/{session_id};
    //    the program token is the absolute mock-session path. The dummy shells the
    //    same `spt` to bind (via --spt). ──
    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
    let self_cmd = format!(
        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
    );
    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only
    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —
    // the online signal is gated to the reconcile host predicate). A LONG-LIVED psyche
    // (a copy of `spt` run as `ready {id}`, mirroring brain_restart_psyche_dup) so the
    // brain's reconcile hosts a resident `{id}-psyche` — the same shape the Wave 2
    // VIEWER-CLOSE / psyche-reap gates need.
    let src = perch::spt_home().join("srcs").join("dummyharness");
    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=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='psychebin ready {{id}}'\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 REAL daemon broker process. ──
    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: brain never came up.\n{}",
                std::fs::read_to_string(&brain_log).unwrap_or_default()
            );
        }
    };

    // ── (4) The REAL spt-hosted bringup: endpoint run --start (broker spawns the
    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──
    let id = "dummy7";
    let run = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(45))
    };

    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);

    // The dummy binds its perch live within a few seconds of the broker PTY spawn.
    let mut online = false;
    let deadline = Instant::now() + Duration::from_secs(20);
    while Instant::now() < deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
            online = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(150));
    }

    // The broker-spawned harness pid (from the terse `ENDPOINT_RUN:… pid=NNN`
    // machine token) — the long-lived PTY child itself, NOT the transient `spt api
    // bind` subprocess that stamps info.json's pid. It must stay alive (heartbeating).
    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
    let harness_pid: Option<u32> = run_stderr
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());
    let alive_after = {
        std::thread::sleep(Duration::from_millis(700));
        harness_pid.map(spt_store::proc::is_process_alive).unwrap_or(false)
    };

    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──
    let rc_err = home.path().join("rc.stderr.log");
    let rc_err_file = std::fs::File::create(&rc_err).expect("create rc stderr log");
    let mut rc: Child = Command::new(&spt_bin)
        .no_window()
        .args(["rc", id])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::piped())
        .stderr(Stdio::from(rc_err_file))
        .stdin(Stdio::piped()) // keep stdin OPEN — a closed stdin makes the rc pump EOF-detach before streaming
        .spawn()
        .expect("spawn spt rc <id>");
    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window
    let mut rc_out = rc.stdout.take().unwrap();
    let (tx, rx) = std::sync::mpsc::channel::<String>();
    std::thread::spawn(move || {
        let mut buf = [0u8; 4096];
        let mut acc = String::new();
        loop {
            match rc_out.read(&mut buf) {
                Ok(0) => break,
                Ok(n) => {
                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));
                    if acc.contains("DUMMY_HARNESS_TICK") {
                        let _ = tx.send(acc.clone());
                        break;
                    }
                }
                Err(_) => break,
            }
        }
        let _ = tx.send(acc);
    });
    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();
    let rc_saw_tick = rc_captured.contains("DUMMY_HARNESS_TICK");

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();
    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did
    // NOT RC_FAIL (no "no live session" / loopback-dial refusal).
    let rc_connected = rc_stderr.contains("PUMP_IPC_READER") && !rc_stderr.contains("RC_FAIL");
    eprintln!(
        "=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \
         alive_after={alive_after} rc_connected={rc_connected} rc_saw_tick={rc_saw_tick} \
         rc_stdout_bytes={} ===\n=== endpoint run stderr ===\n{}\n=== rc stderr ===\n{rc_stderr}\
         \n=== rc stdout (first 400) ===\n{}",
        run.status.success(),
        rc_captured.len(),
        String::from_utf8_lossy(&run.stderr),
        rc_captured.chars().take(400).collect::<String>(),
    );

    // ── (6) Reap scoped: kill rc, the harness, the brain, daemon-stop, broker. ──
    kill_pid(rc.id());
    let _ = rc.kill();
    let _ = rc.wait();
    if let Some(p) = harness_pid {
        kill_pid(p);
    }
    // The reconcile may have hosted a `{id}-psyche` — reap it too (scoped).
    let psyche_perch =
        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
        kill_pid(p);
    }
    let _ = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(["daemon", "stop"]).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(20))
    };
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();

    assert!(
        run.status.success(),
        "endpoint run --start must succeed: {}",
        String::from_utf8_lossy(&run.stderr)
    );
    assert!(
        online,
        "the dummy harness must bind its perch ONLINE via the real broker PTY bringup.\n\
         === brain stderr ===\n{brain_stderr}"
    );
    assert!(
        alive_after,
        "the dummy harness PTY child must STAY ALIVE (heartbeating), not exit like the \
         default mock — harness_pid={harness_pid:?}"
    );
    // `rc_connected` (PUMP_IPC_READER present, no RC_FAIL) was the Wave-1 proxy for
    // "the attach wired up." It is now SUBSUMED by the `rc_saw_tick` keystone gate
    // below: receiving the harness's live output is a strictly stronger proof of a
    // wired attach than the setup log line (and the log line races the now-fast
    // teardown — rc delivers a tick and exits before its stderr file flushes). Kept
    // informational in the DIAGNOSTIC above; the hard proof is output delivery.
    let _ = rc_connected;
    // KEYSTONE GATE (v0.12.1 Wave 2 L0): a clean `spt rc` attach to this LIVE,
    // ALREADY-PRODUCING (non-empty OutputLog ring) endpoint-run harness DELIVERS its
    // `[session.self]` stdout — the operator's "attach shows no output" is fixed. This
    // is the exact case the v0.12.0 mock suite + the loopback E2Es lacked (they only
    // ever attach to an idle/just-spawned child, so `become_controller` replays an
    // empty ring and never triggers the replay-vs-forward IPC deadlock). The fix
    // (serve_attach forwards on a SEPARATE broker conn from the one it receives the
    // replay on) is what makes this pass.
    // [int->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
    assert!(
        rc_saw_tick,
        "`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \
         output (got {} bytes) — the broker must forward an already-producing harness's \
         PTY to the attach without the replay-vs-forward deadlock.\n=== rc stderr ===\n{rc_stderr}\
         \n=== brain stderr ===\n{brain_stderr}",
        rc_captured.len()
    );
}

// ── REPRO-FIRST regression for REQ-HAZARD-RC-ATTACH-ONLINE-RACE ──────────────
// THE BUG (doyle root-caused): `spt endpoint run` in an ATTACH terminal action
// (no `--start`) attaches BEFORE the freshly-spawned endpoint is online. In
// `cmd_endpoint_run` the flow is `launch_harness_brokered_in` (returns once the
// harness PROCESS is spawned) → `run_attach`, with NO await-online between them.
// The broker-PTY bind (info status→STATUS_ONLINE + a live session) lands ASYNC,
// so the immediate `run_attach` loses the race: it either short-circuits on the
// offline status-gate ("offline — nothing to attach", REQ-HAZARD-RC-ATTACH-FAILFAST
// in rc.rs run_attach) or hits "no live session for endpoint" (printed as
// RC_FAIL) — never connecting to the live PTY, so NO heartbeat is ever seen.
//
// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live
// DUMMY_HARNESS_TICK — proof the attach waited for bringup and connected to the
// live session — exactly as the --start + separate `rc` path above does.
//
// Pre-fix this test is RED: the captured output shows the offline/no-live-session
// bail and NO heartbeat. (The fix — await-online between launch and run_attach —
// lands in a later step; this commit touches ONLY this test file.)
//
// Same isolated-SPT_HOME + dummyharness fixture + scoped full-tree reap as the
// sister test above; the ONLY behavioural difference is dropping `--start` so the
// SAME process runs both bringup AND attach (the bug surface), captured bounded.
// [int->REQ-HAZARD-RC-ATTACH-ONLINE-RACE]
#[test]
fn endpoint_run_attach_awaits_online_before_attaching() {
    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attach rides a broker loopback
    //    conn that needs a net-bound broker — identical to the sister test). ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    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();
    let mock_session = sibling_bin("mock-session");
    if !mock_session.exists() {
        panic!(
            "the dummy-harness program must be built: {} \
             (pre-build: cargo build -p mock-adapter --bin mock-session)",
            mock_session.display()
        );
    }

    // ── (2) Register the DUMMY-HARNESS adapter (verbatim shape from the sister
    //    test: long-lived kind=harness, heartbeats, binds its own perch; a
    //    resident psyche so the brain's reconcile marks status=online). ──
    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
    let self_cmd = format!(
        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
    );
    let src = perch::spt_home().join("srcs").join("dummyharness");
    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=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='psychebin ready {{id}}'\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 REAL daemon broker process. ──
    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: brain never came up.\n{}",
                    std::fs::read_to_string(&brain_log).unwrap_or_default()
                );
            }
        };

    let id = "dummyrace1";
    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);

    // ── (4) DETERMINISTIC OFFLINE PRECONDITION. The bug only surfaces reliably when
    //    the SAME id is already KNOWN-OFFLINE at the moment of the attach-by-default
    //    run, so `run_attach`'s status-gate short-circuits ("offline — nothing to
    //    attach") pre-fix instead of racing a fresh bind (a fresh id can win the race
    //    far enough to connect-and-tick — a FALSE green). So: (4a) bring the endpoint
    //    up cleanly with --start, (4b) wait ONLINE, (4c) kill the harness, (4d) poll
    //    until the broker's liveness reconcile (reconcile_hosted_liveness, livehost.rs)
    //    stamps the perch OFFLINE — that dead-but-offline endpoint is the precondition
    //    that makes the next attach deterministically hit the offline gate pre-fix. ──

    // (4a) Clean --start bringup (captured bounded; this run returns once the harness
    //      process is spawned — its `pid=` token names the FIRST harness incarnation).
    let start_run = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(45))
    };
    let start_stderr = String::from_utf8_lossy(&start_run.stderr).to_string();
    let harness_pid_1: Option<u32> = start_stderr
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());

    // (4b) Wait for the dummy to bind its perch ONLINE via the real broker PTY.
    let mut online_1 = false;
    let deadline = Instant::now() + Duration::from_secs(20);
    while Instant::now() < deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
            online_1 = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(150));
    }

    // (4c) Drive the endpoint OFFLINE: kill the first harness incarnation. Its broker
    //      session then drains and the next reconcile sweep sees no live session for a
    //      dead, online-latched, controllable perch → marks it offline.
    if let Some(p) = harness_pid_1 {
        kill_pid(p);
    }

    // (4d) POLL info.status until STATUS_OFFLINE (bounded). This is the precondition:
    //      the attach-by-default run below will read this still-OFFLINE status pre-fix.
    let mut went_offline = false;
    let off_deadline = Instant::now() + Duration::from_secs(15);
    while Instant::now() < off_deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_OFFLINE) {
            went_offline = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(150));
    }

    // ── (5) THE BUG SURFACE: `endpoint run` with NO `--start`, SAME id = attach-by-
    //    default re-bringup. The SAME process re-spawns the harness AND then runs the
    //    attach pump. Pre-fix the immediate attach reads the still-OFFLINE status →
    //    "offline — nothing to attach" → NO tick. Post-fix (await_endpoint_online)
    //    waits for the re-bringup to bind ONLINE → attach → tick delivered.
    //
    //    Spawn as a Child with a HELD write-end of stdin (a closed stdin makes the
    //    attach pump's stdin-reader hit EOF and CLEAN-DETACH before the 200ms
    //    heartbeat — output_bounded/cmd.output() closes stdin, so it can NEVER observe
    //    a tick, in BOTH pre- and post-fix runs). Stream stdout in a thread until
    //    DUMMY_HARNESS_TICK (the heartbeat-delivery keystone), exactly as the sister
    //    test's `rc` attach does. ──
    let run_err = home.path().join("run.stderr.log");
    let run_err_file = std::fs::File::create(&run_err).expect("create endpoint-run stderr log");
    let mut run: Child = Command::new(&spt_bin)
        .no_window()
        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::piped())
        .stderr(Stdio::from(run_err_file))
        .stdin(Stdio::piped()) // HOLD stdin open — a closed stdin EOF-detaches before the tick
        .spawn()
        .expect("spawn endpoint run (no --start, attach-by-default)");
    let _run_stdin = run.stdin.take(); // hold the write end open for the attach window
    let mut run_out = run.stdout.take().unwrap();
    let (tx, rx) = std::sync::mpsc::channel::<String>();
    std::thread::spawn(move || {
        let mut buf = [0u8; 4096];
        let mut acc = String::new();
        loop {
            match run_out.read(&mut buf) {
                Ok(0) => break,
                Ok(n) => {
                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));
                    if acc.contains("DUMMY_HARNESS_TICK") {
                        let _ = tx.send(acc.clone());
                        break;
                    }
                }
                Err(_) => break,
            }
        }
        let _ = tx.send(acc);
    });
    let run_captured = rx.recv_timeout(Duration::from_secs(12)).unwrap_or_default();

    // THE CLAIM (compute pre-reap, assert post-reap): the attach awaited the re-
    // bringup coming ONLINE, connected to the live session, and delivered the
    // harness's live heartbeat — the SAME live-PTY marker the sister test asserts.
    let saw_tick = run_captured.contains("DUMMY_HARNESS_TICK");
    let run_stderr_txt = std::fs::read_to_string(&run_err).unwrap_or_default();
    // Diagnostic discriminators for the pre-fix race losses (info only).
    let bailed_offline = run_captured.contains("offline — nothing to attach")
        || run_stderr_txt.contains("offline — nothing to attach");
    let bailed_no_session =
        run_captured.contains("no live session") || run_stderr_txt.contains("no live session");

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!(
        "=== DIAGNOSTIC (ONLINE-RACE): online_1={online_1} went_offline={went_offline} \
         saw_tick={saw_tick} bailed_offline={bailed_offline} bailed_no_session={bailed_no_session} \
         run_stdout_bytes={} ===\n=== endpoint run stdout (first 800) ===\n{}\n\
         === endpoint run stderr ===\n{run_stderr_txt}\n=== brain stderr ===\n{brain_stderr}",
        run_captured.len(),
        run_captured.chars().take(800).collect::<String>(),
    );

    // ── (6) Reap the ENTIRE tree SCOPED before asserting (never leave a stray spt.exe
    //    — a survivor locks target\debug\spt.exe → EPERM next checkout). The endpoint
    //    is brought up TWICE, so reap BOTH harness incarnations. Then the re-bringup
    //    attach process (`run`), the reconcile-hosted psyche, the brain, daemon-stop,
    //    broker — same order as the sister test. ──
    let harness_pid_2: Option<u32> = run_stderr_txt
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());
    kill_pid(run.id());
    let _ = run.kill();
    let _ = run.wait();
    if let Some(p) = harness_pid_1 {
        kill_pid(p);
    }
    if let Some(p) = harness_pid_2 {
        kill_pid(p);
    }
    let psyche_perch =
        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
        kill_pid(p);
    }
    let _ = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args(["daemon", "stop"])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(20))
    };
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();
    // Poll-verify the broker + both harness incarnations are reaped (scoped).
    let reap_deadline = Instant::now() + Duration::from_secs(10);
    while Instant::now() < reap_deadline {
        let brain_gone = !spt_store::proc::is_process_alive(brain_pid);
        let h1_gone = harness_pid_1
            .map(|p| !spt_store::proc::is_process_alive(p))
            .unwrap_or(true);
        let h2_gone = harness_pid_2
            .map(|p| !spt_store::proc::is_process_alive(p))
            .unwrap_or(true);
        if brain_gone && h1_gone && h2_gone {
            break;
        }
        std::thread::sleep(Duration::from_millis(100));
    }

    // Precondition sanity (only meaningful if the env actually drove the endpoint
    // offline; if not, the test would be vacuous — surface it loudly rather than fake).
    assert!(
        online_1,
        "PRECONDITION: the --start bringup must bind ONLINE before we drive it offline.\n\
         === brain stderr ===\n{brain_stderr}"
    );
    assert!(
        went_offline,
        "PRECONDITION: killing the harness must drive the perch OFFLINE (broker liveness \
         reconcile) so the attach-by-default run deterministically hits the offline gate \
         pre-fix.\n=== brain stderr ===\n{brain_stderr}"
    );

    // ── (7) THE GATE. Pre-fix: RED (attach read the still-OFFLINE status, bailed
    //    "offline — nothing to attach", no heartbeat). Post-fix (await_endpoint_online
    //    before run_attach): GREEN (the re-bringup binds ONLINE, the attach connects,
    //    the heartbeat is delivered). ──
    assert!(
        saw_tick,
        "`spt endpoint run` WITHOUT --start (attach-by-default) against a known-OFFLINE id \
         must await the re-spawned endpoint coming ONLINE before attaching, then DELIVER its \
         live DUMMY_HARNESS_TICK — instead the immediate attach read the still-OFFLINE status \
         and bailed (bailed_offline={bailed_offline} bailed_no_session={bailed_no_session}, \
         {} stdout bytes, no heartbeat).\n=== endpoint run stdout ===\n{run_captured}\
         \n=== endpoint run stderr ===\n{run_stderr_txt}\n=== brain stderr ===\n{brain_stderr}",
        run_captured.len(),
    );
}


I now have a thorough understanding of the test patterns. Let me verify the picker model implementation (already modified per the prompt) to confirm what's already there, plus check the cli.rs cwd plumbing, then verify the workspace compiles before adding tests.


1290	    let adapters_dir = spt_store::perch::adapters_dir();
1291	    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
1292	        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
1293	        Ok(_) => {
1294	            eprintln!(
1295	                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
1296	                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
1297	            );
1298	            return 1;
1299	        }
1300	        Err(e) => {
1301	            eprintln!(
1302	                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
1303	                 harness adapter / valid profile on this node ({e}; spt adapter list)"
1304	            );
1305	            return 1;
1306	        }
1307	    };
1308
1309	    // A fresh bringup mints a provisional spawn-time session id; --resume carries
1310	    // a prior one (the harness reports the real id post-spawn either way).
1311	    // `is_resume` selects the adapter's native-resume command (REQ-SESSION-RESUME-TEMPLATE).
1312	    let is_resume = resume.is_some();
1313	    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);
1314
1315	    if let Err(e) = spt_daemon::daemon::ensure_running() {
1316	        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
1317	        return 1;
1318	    }
1319	    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
1320	    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
1321	    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
1322	    // session history, and digest source relative to cwd.
1323	    //
1324	    // Resume cwd fallback chain (REQ-SESSION-RESUME-TEMPLATE): a caller-supplied
1325	    // cwd (the picker threads the selected Resume row's recorded cwd) → on a
1326	    // resume, the perch's `info.cwd` (W3 bind-cwd) → `current_dir` (today's
1327	    // single-project behavior + pre-migration ledger rows). A fresh bringup never
1328	    // consults `info.cwd` — it always lands in `current_dir` as before. A failure
1329	    // to read the cwd falls back to the broker-inherited cwd (None).
1330	    let project_cwd = cwd
1331	        .or_else(|| {
1332	            is_resume
1333	                .then(|| {
1334	                    let perch =
1335	                        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
1336	                    spt_store::info::read_info(&perch).and_then(|r| r.cwd)
1337	                })
1338	                .flatten()
1339	        })
1340	        .or_else(|| {
1341	            std::env::current_dir()
1342	                .ok()
1343	                .map(|p| p.to_string_lossy().into_owned())
1344	        });
1345	    match spt_daemon::harnesshost::launch_harness_brokered_in(
1346	        &spt_daemon::endpoint::broker_socket_name(),
1347	        id,
1348	        adapter,
1349	        &session_id,
1350	        &manifest,
1351	        project_cwd.as_deref(),
1352	        is_resume,
1353	    ) {
1354	        Ok(spawned) => {
1355	            // A TERSE machine line (no Rust `Some(..)` Debug leak, no "binds its
1356	            // perch on startup" internals — W4 UX: user-friendly, not a process
1357	            // log). `pid=<n>` stays a parseable machine token (scripts + e2e read
1358	            // the long-lived PTY child off it); a backend without a pid prints `-`.
1359	            let pid_display = spawned
1360	                .pid
1361	                .map(|p| p.to_string())
1362	                .unwrap_or_else(|| "-".to_string());
1363	            eprintln!("ENDPOINT_RUN:{id} adapter={adapter} session={session_id} pid={pid_display}");
1364	        }
1365	        Err(e) => {
1366	            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
1367	            return 1;
1368	        }
1369	    }
1370
1371	    // Terminal action: start (return now) | view (read-only attach) | attach
1372	    // (default). Attach/view route through the SAME rc pump as `spt rc`.
1373	    if start {
1374	        // `ENDPOINT_RUN_STARTED:{id}` is the machine token (no Markdown markers →
1375	        // survives strip intact for a piped adapter); the `spt rc` hint restyles
1376	        // on a tty and strips clean otherwise. [impl->REQ-CLI-OUTPUT-MARKDOWN]
1377	        eprintln!(
1378	            "{}",
1379	            crate::helpfmt::render(
1380	                &format!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)"),
1381	                crate::helpfmt::stderr_color()
1382	            )
1383	        );
1384	        return 0;
1385	    }
1386	    // Await the bringup ONLINE before attaching (REQ-HAZARD-RC-ATTACH-ONLINE-RACE):
1387	    // launch_harness_brokered_in returns once the harness PROCESS is spawned, but the
1388	    // broker-PTY bind (status -> ONLINE + a live session) lands ASYNC. An immediate
1389	    // run_attach loses the race three ways — connects to an empty ring then
1390	    // EOF-detaches, hits the offline status-gate, or finds no live session yet. FAIL
1391	    // LOUD on timeout rather than fall through to a doomed attach. Only the
1392	    // offline-bringup (Run) path reaches here; an already-online endpoint routes
1393	    // Outcome::Attach -> rc::run_attach and never double-counts this wait.
1394	    if !await_endpoint_online(id) {
1395	        eprintln!(
1396	            "ENDPOINT_RUN_ONLINE_TIMEOUT:{id}: harness did not come online within {ONLINE_AWAIT_SECS}s \
1397	             — not attaching (check `spt endpoint list`; retry with `spt rc {id}`)"
1398	        );
1399	        return 1;
1400	    }
1401	    match crate::rc::run_attach(id, rc_intent(view, false)) {
1402	        Ok(()) => 0,
1403	        Err(e) => {
1404	            eprintln!("RC_FAIL:{id}: {e}");
1405	            1
1406	        }
1407	    }
1408	}
1409

    let adapters_dir = spt_store::perch::adapters_dir();
    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
        Ok(_) => {
            eprintln!(
                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
            );
            return 1;
        }
        Err(e) => {
            eprintln!(
                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
                 harness adapter / valid profile on this node ({e}; spt adapter list)"
            );
            return 1;
        }
    };

    // A fresh bringup mints a provisional spawn-time session id; --resume carries
    // a prior one (the harness reports the real id post-spawn either way).
    // `is_resume` selects the adapter's native-resume command (REQ-SESSION-RESUME-TEMPLATE).
    let is_resume = resume.is_some();
    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);

    if let Err(e) = spt_daemon::daemon::ensure_running() {
        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
        return 1;
    }
    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
    // session history, and digest source relative to cwd.
    //
    // Resume cwd fallback chain (REQ-SESSION-RESUME-TEMPLATE): a caller-supplied
    // cwd (the picker threads the selected Resume row's recorded cwd) → on a
    // resume, the perch's `info.cwd` (W3 bind-cwd) → `current_dir` (today's
    // single-project behavior + pre-migration ledger rows). A fresh bringup never
    // consults `info.cwd` — it always lands in `current_dir` as before. A failure
    // to read the cwd falls back to the broker-inherited cwd (None).
    let project_cwd = cwd
        .or_else(|| {
            is_resume
                .then(|| {
                    let perch =
                        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
                    spt_store::info::read_info(&perch).and_then(|r| r.cwd)
                })
                .flatten()
        })
        .or_else(|| {
            std::env::current_dir()
                .ok()
                .map(|p| p.to_string_lossy().into_owned())
        });
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
        project_cwd.as_deref(),
        is_resume,
    ) {
        Ok(spawned) => {
            // A TERSE machine line (no Rust `Some(..)` Debug leak, no "binds its
            // perch on startup" internals — W4 UX: user-friendly, not a process
            // log). `pid=<n>` stays a parseable machine token (scripts + e2e read
            // the long-lived PTY child off it); a backend without a pid prints `-`.
            let pid_display = spawned
                .pid
                .map(|p| p.to_string())
                .unwrap_or_else(|| "-".to_string());
            eprintln!("ENDPOINT_RUN:{id} adapter={adapter} session={session_id} pid={pid_display}");
        }
        Err(e) => {
            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
            return 1;
        }
    }

    // Terminal action: start (return now) | view (read-only attach) | attach
    // (default). Attach/view route through the SAME rc pump as `spt rc`.
    if start {
        // `ENDPOINT_RUN_STARTED:{id}` is the machine token (no Markdown markers →
        // survives strip intact for a piped adapter); the `spt rc` hint restyles
        // on a tty and strips clean otherwise. [impl->REQ-CLI-OUTPUT-MARKDOWN]
        eprintln!(
            "{}",
            crate::helpfmt::render(
                &format!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)"),
                crate::helpfmt::stderr_color()
            )
        );
        return 0;
    }
    // Await the bringup ONLINE before attaching (REQ-HAZARD-RC-ATTACH-ONLINE-RACE):
    // launch_harness_brokered_in returns once the harness PROCESS is spawned, but the
    // broker-PTY bind (status -> ONLINE + a live session) lands ASYNC. An immediate
    // run_attach loses the race three ways — connects to an empty ring then
    // EOF-detaches, hits the offline status-gate, or finds no live session yet. FAIL
    // LOUD on timeout rather than fall through to a doomed attach. Only the
    // offline-bringup (Run) path reaches here; an already-online endpoint routes
    // Outcome::Attach -> rc::run_attach and never double-counts this wait.
    if !await_endpoint_online(id) {
        eprintln!(
            "ENDPOINT_RUN_ONLINE_TIMEOUT:{id}: harness did not come online within {ONLINE_AWAIT_SECS}s \
             — not attaching (check `spt endpoint list`; retry with `spt rc {id}`)"
        );
        return 1;
    }
    match crate::rc::run_attach(id, rc_intent(view, false)) {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("RC_FAIL:{id}: {e}");
            1
        }
    }
}


Found 4 files
crates\spt\src\picker\mod.rs
crates\spt\src\picker\view.rs
crates\spt\src\picker\model.rs
crates\spt\src\picker\data.rs


The cli.rs cwd fallback chain matches the design. Let me read the picker model.rs (resume_outcome, ResumeRow::title, and the existing `resume_outcome_bakes_session` unit that was updated) to confirm the picker units already pass and don't need additions per the prompt.


120	            adapter: a.name.clone(),
121	            profile: None,
122	            is_leaf: false,
123	        });
124	        for p in a.shipped.iter().chain(a.local.iter()) {
125	            rows.push(AdapterOption {
126	                adapter: a.name.clone(),
127	                profile: Some(p.clone()),
128	                is_leaf: true,
129	            });
130	        }
131	    }
132	    rows
133	}
134
135	/// One prior session from the per-endpoint ledger, rendered as a Resume option.
136	/// Built from `spt_store::sessions::SessionEntry` in `data.rs`.
137	#[derive(Debug, Clone, PartialEq, Eq)]
138	pub struct ResumeRow {
139	    pub session_id: String,
140	    pub ts: String,
141	    /// The boundary token (`boot`/`clear`/`compact`) — the title fallback when
142	    /// no project association exists (the ledger row carries no project).
143	    pub trigger: String,
144	    /// Best-effort project name (empty if none derivable).
145	    pub project: String,
146	    /// The session's recorded project cwd (ledger `cwd`, REQ-SESSION-RESUME-TEMPLATE).
147	    /// `None` for a pre-migration row → resume falls back to info.cwd/current_dir.
148	    pub cwd: Option<String>,
149	}
150
151	impl ResumeRow {
152	    /// The option title: `<project|trigger> - <local_time> (…<id5>)` (picker
153	    /// spec + design D). `local_time` renders the stored RFC3339-UTC `ts` in the
154	    /// operator's LOCAL timezone for human legibility.
155	    pub fn title(&self) -> String {
156	        let head = if self.project.is_empty() {
157	            &self.trigger
158	        } else {
159	            &self.project
160	        };
161	        let id5: String = {
162	            let s = &self.session_id;
163	            let n = s.chars().count();
164	            s.chars().skip(n.saturating_sub(5)).collect()
165	        };
166	        format!("{head} - {} (…{id5})", Self::local_time(&self.ts))
167	    }
168
169	    /// Render an RFC3339-UTC `ts` as a local-time human stamp
170	    /// `HH:MM<AM|PM> <offset> MM-DD-YYYY` (e.g. `03:45PM -0800 06-19-2026`). The
171	    /// tz field is the numeric UTC offset (`%z`): chrono carries no reliable
172	    /// short tz-abbreviation for `Local` across platforms (Windows `%Z` emits a
173	    /// long name like "Pacific Standard Time"), so the offset is the
174	    /// deterministic, operator-confirmable form (design D, TZ-caveat resolve).
175	    /// A parse failure degrades to the raw `ts` — the picker degrades, never
176	    /// panics.
177	    // [impl->REQ-SESSION-RESUME-TEMPLATE]
178	    fn local_time(ts: &str) -> String {
179	        match chrono::DateTime::parse_from_rfc3339(ts) {
180	            Ok(dt) => dt
181	                .with_timezone(&chrono::Local)
182	                .format("%I:%M%p %z %m-%d-%Y")
183	                .to_string(),
184	            Err(_) => ts.to_string(),
185	        }
186	    }
187	}
188
189	/// A pick-existing category. Left/right cycles through these.
190	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191	pub enum Category {
192	    /// Endpoints scoped to the cwd project.
193	    Project,
194	    /// This node's local perches.
195	    Local,
196	    /// Subnet endpoints.
197	    Subnet,
198	}
199
200	impl Category {
201	    /// Display label (the project category carries the cwd project name).
202	    pub fn label(self, project_name: &str) -> String {
203	        match self {
204	            Category::Project => format!("{project_name} project"),
205	            Category::Local => "Local node".to_string(),
206	            Category::Subnet => "Subnet".to_string(),
207	        }
208	    }
209
210	    /// The ordered category ring for left/right navigation.
211	    pub const ALL: [Category; 3] = [Category::Project, Category::Local, Category::Subnet];
212	}
213
214	/// One discovered endpoint row (gathered in `data.rs`).
215	#[derive(Debug, Clone, PartialEq, Eq)]
216	pub struct EndpointRow {
217	    pub id: String,
218	    /// Grouping label within the category (e.g. "local", "subnet:node",
219	    /// project name) — rows render grouped then alpha-sorted.
220	    pub group: String,
221	    pub node: String,
222	    pub status: EpStatus,
223	    /// `true` if this endpoint lives on THIS node (gates Resume-from-history,
224	    /// which is offline+LOCAL only).
225	    pub is_local: bool,
226	    /// The right-pane description: harness `adapter:profile`.
227	    pub adapter_profile: String,
228	    /// Project history newest→oldest (best-effort; empty if none).
229	    pub project_history: Vec<String>,
230	    /// The `spt endpoint description` blurb (empty if none).
231	    pub description: String,
232	    /// Prior sessions for Resume-from-history (only populated for offline+local).
233	    pub resume_rows: Vec<ResumeRow>,
234	    /// The node currently controlling this endpoint's session (its broker-owned
235	    /// `driven_by`), if any — drives the W2.5 status-conditional confirm options
236	    /// (REQ-RCVIEW-1 / REQ-KICK-1): a CONTROLLED endpoint offers View + Kick, not
237	    /// a plain Attach. Local rows read it off `info.json` and render it through the
238	    /// canonical `node_label_display` so the "controlled by" pin shows the NODE NAME
239	    /// (`LABEL (keyprefix…)`), not raw key-hex (REQ-PICKER-4); subnet rows leave it

            adapter: a.name.clone(),
            profile: None,
            is_leaf: false,
        });
        for p in a.shipped.iter().chain(a.local.iter()) {
            rows.push(AdapterOption {
                adapter: a.name.clone(),
                profile: Some(p.clone()),
                is_leaf: true,
            });
        }
    }
    rows
}

/// One prior session from the per-endpoint ledger, rendered as a Resume option.
/// Built from `spt_store::sessions::SessionEntry` in `data.rs`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResumeRow {
    pub session_id: String,
    pub ts: String,
    /// The boundary token (`boot`/`clear`/`compact`) — the title fallback when
    /// no project association exists (the ledger row carries no project).
    pub trigger: String,
    /// Best-effort project name (empty if none derivable).
    pub project: String,
    /// The session's recorded project cwd (ledger `cwd`, REQ-SESSION-RESUME-TEMPLATE).
    /// `None` for a pre-migration row → resume falls back to info.cwd/current_dir.
    pub cwd: Option<String>,
}

impl ResumeRow {
    /// The option title: `<project|trigger> - <local_time> (…<id5>)` (picker
    /// spec + design D). `local_time` renders the stored RFC3339-UTC `ts` in the
    /// operator's LOCAL timezone for human legibility.
    pub fn title(&self) -> String {
        let head = if self.project.is_empty() {
            &self.trigger
        } else {
            &self.project
        };
        let id5: String = {
            let s = &self.session_id;
            let n = s.chars().count();
            s.chars().skip(n.saturating_sub(5)).collect()
        };
        format!("{head} - {} (…{id5})", Self::local_time(&self.ts))
    }

    /// Render an RFC3339-UTC `ts` as a local-time human stamp
    /// `HH:MM<AM|PM> <offset> MM-DD-YYYY` (e.g. `03:45PM -0800 06-19-2026`). The
    /// tz field is the numeric UTC offset (`%z`): chrono carries no reliable
    /// short tz-abbreviation for `Local` across platforms (Windows `%Z` emits a
    /// long name like "Pacific Standard Time"), so the offset is the
    /// deterministic, operator-confirmable form (design D, TZ-caveat resolve).
    /// A parse failure degrades to the raw `ts` — the picker degrades, never
    /// panics.
    // [impl->REQ-SESSION-RESUME-TEMPLATE]
    fn local_time(ts: &str) -> String {
        match chrono::DateTime::parse_from_rfc3339(ts) {
            Ok(dt) => dt
                .with_timezone(&chrono::Local)
                .format("%I:%M%p %z %m-%d-%Y")
                .to_string(),
            Err(_) => ts.to_string(),
        }
    }
}

/// A pick-existing category. Left/right cycles through these.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
    /// Endpoints scoped to the cwd project.
    Project,
    /// This node's local perches.
    Local,
    /// Subnet endpoints.
    Subnet,
}

impl Category {
    /// Display label (the project category carries the cwd project name).
    pub fn label(self, project_name: &str) -> String {
        match self {
            Category::Project => format!("{project_name} project"),
            Category::Local => "Local node".to_string(),
            Category::Subnet => "Subnet".to_string(),
        }
    }

    /// The ordered category ring for left/right navigation.
    pub const ALL: [Category; 3] = [Category::Project, Category::Local, Category::Subnet];
}

/// One discovered endpoint row (gathered in `data.rs`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EndpointRow {
    pub id: String,
    /// Grouping label within the category (e.g. "local", "subnet:node",
    /// project name) — rows render grouped then alpha-sorted.
    pub group: String,
    pub node: String,
    pub status: EpStatus,
    /// `true` if this endpoint lives on THIS node (gates Resume-from-history,
    /// which is offline+LOCAL only).
    pub is_local: bool,
    /// The right-pane description: harness `adapter:profile`.
    pub adapter_profile: String,
    /// Project history newest→oldest (best-effort; empty if none).
    pub project_history: Vec<String>,
    /// The `spt endpoint description` blurb (empty if none).
    pub description: String,
    /// Prior sessions for Resume-from-history (only populated for offline+local).
    pub resume_rows: Vec<ResumeRow>,
    /// The node currently controlling this endpoint's session (its broker-owned
    /// `driven_by`), if any — drives the W2.5 status-conditional confirm options
    /// (REQ-RCVIEW-1 / REQ-KICK-1): a CONTROLLED endpoint offers View + Kick, not
    /// a plain Attach. Local rows read it off `info.json` and render it through the
    /// canonical `node_label_display` so the "controlled by" pin shows the NODE NAME
    /// (`LABEL (keyprefix…)`), not raw key-hex (REQ-PICKER-4); subnet rows leave it


The picker model is already fully implemented. Per the prompt, the picker units were already updated by the implementer and I should verify they pass rather than duplicate. Let me check the `Outcome::Run` definition and the existing `resume_outcome_bakes_session` unit to confirm.