1138	[[requirements]]
1139	id = "REQ-HAZARD-VIEWER-CLOSE-DETACH"
1140	title = "A VIEW is independent from the endpoint: closing the tab/window where `spt endpoint run` was invoked must detach ONLY the `spt rc` attach pump — the daemon-hosted harness keeps running and stays re-attachable via `spt rc <id>`. ROOT (Windows, v0.12.0 real-harness defect): the daemon never breaks away from the launching terminal's Job Object. Windows Terminal / VS Code place the launched shell AND every descendant into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the terminal's last job handle → the OS terminates every process still in that job. A child escapes only with CREATE_BREAKAWAY_FROM_JOB — used NOWHERE in the tree. Both daemon spawn paths (daemon.rs:707 detached_no_inherit = DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; deelevate.rs:519 elevated = CREATE_NEW_CONSOLE|...) drop the CONSOLE but NOT job membership, so the daemon's freshly broker-spawned ConPTY harness subtree is reaped on tab-close. The ConPTY/pseudoconsole isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon; no console signal / handle leak) — the leaking lifetime binding is the Job Object, not the console. FIX: add CREATE_BREAKAWAY_FROM_JOB to both daemon spawn paths AND pin each broker-spawned harness into a DAEMON-OWNED Job Object (mirror reap.rs/Breap) as backstop (survives even where a terminal sets SILENT_BREAKAWAY_OK=false). Unix: the daemon's own session detachment (new session, no controlling terminal) already keeps a closing terminal's SIGHUP off its children — verify, add a guard test, no code expected. (v0.12.1)"
1141	required_stages = ["doc"]  # doc by doyle (CONTEXT.md "A view is independent from the endpoint" invariant). todlando activates [doc,impl,unit,int] when building: impl = CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths + a daemon-owned Job Object holding each broker-spawned harness; unit = the daemon spawn flags include breakaway + a harness is assigned to the daemon job; int = REAL detached daemon spawned UNDER a parent-held KILL_ON_JOB_CLOSE job → endpoint run a dummy harness → close the parent job → assert the harness pid stays ALIVE + `spt rc <id>` re-attaches + a brand-new endpoint launches (the dummy-harness fixture, NOT a mock)
1142	[[requirements]]
1143	id = "REQ-HAZARD-ATTACH-WEDGE"
1144	title = "A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock (this is data-plane backpressure + worker-pool exhaustion). FIX: loopback sends fail-fast — a full-buffer / BrokenPipe loopback write is an ordinary per-stream error that ENDS serve_attach so one dead stream can never hold a runtime worker (defense-in-depth: raise worker count). Folds the status=online sub-check: a dead spt-hosted endpoint must be marked OFFLINE within one reconcile tick on abrupt child death (confirm the broker session reaps so B2 sees it absent). (v0.12.1)"
1145	required_stages = []  # registry-first; todlando activates [impl,unit,int] when building. int = dummy-harness fixture on a REAL detached daemon: kill the child abruptly + drop the operator pump without a clean detach → assert a NEW endpoint is still served, brain.sessions() returns promptly, the dead endpoint is offlined within a tick, and `daemon stop` completes bounded
1146	[[requirements]]
1147	id = "REQ-PICKER-HISTORY-FRESH"
1148	title = "The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)"
1149	required_stages = []  # registry-first; investigate-then-activate. Likely [impl,unit] once root is pinned
1150	[[requirements]]
1151	id = "REQ-PICKER-ONLINE-ACTION"
1152	title = "The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1)"
1153	required_stages = []  # registry-first; investigate-then-activate (may partly resolve once REQ-HAZARD-ATTACH-WEDGE / status reconcile land). Likely [impl,unit] once root is pinned
1154	[[requirements]]
1155	id = "REQ-ENDPOINT-LIST-MERGE-LOCAL"
1156	title = "`spt endpoint list` always merges this node's LOCAL (unadvertised) perches into the view; the `--local` flag is REMOVED (operator decision 2026-06-17). Rationale: `spt whoami` is a thin alias of `endpoint list` — a just-online agent running `whoami` must see its OWN perch, or it gets an omitted-self view ('chaos'). FIX: drop the `--local` flag + its `--detail` conflict test + the v0.10.0 REQ-PICKER-5 hint line (cli.rs:1678) + cmd_list_local; the bare list merges local perches into the subnet view; fix the whoami alias path accordingly. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target). (v0.12.1)"
1157	required_stages = []  # registry-first; todlando activates [doc,impl,unit] when building. doc = CONTEXT.md endpoint-list/whoami note; impl = merge-local in cmd_endpoint_list + drop the flag/hint/cmd_list_local; unit = bare list includes a local-unadvertised perch + whoami includes the caller's own just-bound perch
1158

[[requirements]]
id = "REQ-HAZARD-VIEWER-CLOSE-DETACH"
title = "A VIEW is independent from the endpoint: closing the tab/window where `spt endpoint run` was invoked must detach ONLY the `spt rc` attach pump — the daemon-hosted harness keeps running and stays re-attachable via `spt rc <id>`. ROOT (Windows, v0.12.0 real-harness defect): the daemon never breaks away from the launching terminal's Job Object. Windows Terminal / VS Code place the launched shell AND every descendant into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the terminal's last job handle → the OS terminates every process still in that job. A child escapes only with CREATE_BREAKAWAY_FROM_JOB — used NOWHERE in the tree. Both daemon spawn paths (daemon.rs:707 detached_no_inherit = DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; deelevate.rs:519 elevated = CREATE_NEW_CONSOLE|...) drop the CONSOLE but NOT job membership, so the daemon's freshly broker-spawned ConPTY harness subtree is reaped on tab-close. The ConPTY/pseudoconsole isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon; no console signal / handle leak) — the leaking lifetime binding is the Job Object, not the console. FIX: add CREATE_BREAKAWAY_FROM_JOB to both daemon spawn paths AND pin each broker-spawned harness into a DAEMON-OWNED Job Object (mirror reap.rs/Breap) as backstop (survives even where a terminal sets SILENT_BREAKAWAY_OK=false). Unix: the daemon's own session detachment (new session, no controlling terminal) already keeps a closing terminal's SIGHUP off its children — verify, add a guard test, no code expected. (v0.12.1)"
required_stages = ["doc"]  # doc by doyle (CONTEXT.md "A view is independent from the endpoint" invariant). todlando activates [doc,impl,unit,int] when building: impl = CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths + a daemon-owned Job Object holding each broker-spawned harness; unit = the daemon spawn flags include breakaway + a harness is assigned to the daemon job; int = REAL detached daemon spawned UNDER a parent-held KILL_ON_JOB_CLOSE job → endpoint run a dummy harness → close the parent job → assert the harness pid stays ALIVE + `spt rc <id>` re-attaches + a brand-new endpoint launches (the dummy-harness fixture, NOT a mock)
[[requirements]]
id = "REQ-HAZARD-ATTACH-WEDGE"
title = "A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock (this is data-plane backpressure + worker-pool exhaustion). FIX: loopback sends fail-fast — a full-buffer / BrokenPipe loopback write is an ordinary per-stream error that ENDS serve_attach so one dead stream can never hold a runtime worker (defense-in-depth: raise worker count). Folds the status=online sub-check: a dead spt-hosted endpoint must be marked OFFLINE within one reconcile tick on abrupt child death (confirm the broker session reaps so B2 sees it absent). (v0.12.1)"
required_stages = []  # registry-first; todlando activates [impl,unit,int] when building. int = dummy-harness fixture on a REAL detached daemon: kill the child abruptly + drop the operator pump without a clean detach → assert a NEW endpoint is still served, brain.sessions() returns promptly, the dead endpoint is offlined within a tick, and `daemon stop` completes bounded
[[requirements]]
id = "REQ-PICKER-HISTORY-FRESH"
title = "The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)"
required_stages = []  # registry-first; investigate-then-activate. Likely [impl,unit] once root is pinned
[[requirements]]
id = "REQ-PICKER-ONLINE-ACTION"
title = "The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1)"
required_stages = []  # registry-first; investigate-then-activate (may partly resolve once REQ-HAZARD-ATTACH-WEDGE / status reconcile land). Likely [impl,unit] once root is pinned
[[requirements]]
id = "REQ-ENDPOINT-LIST-MERGE-LOCAL"
title = "`spt endpoint list` always merges this node's LOCAL (unadvertised) perches into the view; the `--local` flag is REMOVED (operator decision 2026-06-17). Rationale: `spt whoami` is a thin alias of `endpoint list` — a just-online agent running `whoami` must see its OWN perch, or it gets an omitted-self view ('chaos'). FIX: drop the `--local` flag + its `--detail` conflict test + the v0.10.0 REQ-PICKER-5 hint line (cli.rs:1678) + cmd_list_local; the bare list merges local perches into the subnet view; fix the whoami alias path accordingly. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target). (v0.12.1)"
required_stages = []  # registry-first; todlando activates [doc,impl,unit] when building. doc = CONTEXT.md endpoint-list/whoami note; impl = merge-local in cmd_endpoint_list + drop the flag/hint/cmd_list_local; unit = bare list includes a local-unadvertised perch + whoami includes the caller's own just-bound perch


REQs clear. Read the existing mock harness main.rs — decide extend vs new dummy-harness.


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	        _ => {} // default: the agent-session stand-in below.
41	    }
42
43	    let id = arg(&args, "--id").unwrap_or_else(|| {
44	        eprintln!("mock-session: --id required");
45	        std::process::exit(2);
46	    });
47	    let session_id = arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
48	    let spt = arg(&args, "--spt")
49	        .or_else(|| std::env::var("SPT_BIN").ok())
50	        .unwrap_or_else(|| "spt".to_string());
51	    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.
52	    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.
53	    let endpoint_type = arg(&args, "--type");
54	    let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
55
56	    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.
57	    let mut bind_args = vec![
58	        "api".to_string(),
59	        "--adapter".to_string(),
60	        adapter.clone(),
61	        "bind".to_string(),
62	        id.clone(),
63	        "--set-session-id".to_string(),
64	        session_id.clone(),
65	    ];
66	    if let Some(ty) = &endpoint_type {
67	        bind_args.push("--type".to_string());
68	        bind_args.push(ty.clone());
69	    }
70	    let bind = Command::new(&spt).args(&bind_args).status();
71	    if !matches!(bind, Ok(s) if s.success()) {
72	        eprintln!("mock-session: api bind failed: {bind:?}");
73	        std::process::exit(1);
74	    }
75
76	    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.
77	    let state = Command::new(&spt)
78	        .args([
79	            "api",
80	            "--adapter",
81	            &adapter,
82	            "state",
83	            "idle",
84	            &id,
85	            "--session-id",
86	            &session_id,
87	        ])
88	        .status();
89	    if !matches!(state, Ok(s) if s.success()) {
90	        eprintln!("mock-session: api state idle failed: {state:?}");
91	        std::process::exit(1);
92	    }
93
94	    println!("MOCK_SESSION_READY:{id}");
95	}
96

//! `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;
        }
        _ => {} // 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}");
}


mock-session binds then **exits** — exactly why v0.12.0's mock path never exercised the long-lived broker-PTY-attach lifecycle. Wave 1: add a `dummy` long-lived mode (bind → heartbeat-print → stay alive til killed), the permanent real-harness fixture. Extend mock-session.


Edit

        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;
        }
        _ => {} // default: the agent-session stand-in below.
    }

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


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

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

//! `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;
        }
        _ => {} // 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}");
}

+            // 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."

+            let id = arg(&args, "--id").unwrap_or_else(|| {

+                eprintln!("mock-session --mode dummy: --id required");

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

+            // Bind the perch live (spt-hosted direct bind, mirroring the default

+            // stand-in below). A failure is fatal — the fixture must come up.

+                "--adapter".to_string(),

+                "--set-session-id".to_string(),

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

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

+                println!("DUMMY_HARNESS_TICK:{id}:{n}");

+                let _ = std::io::stdout().flush();

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


Dummy mode added. Now the Wave 1 fixture test driving REAL daemon + `endpoint run` + `rc` attach. Check existing real-daemon + rc-attach test patterns to mirror.


Bash

cd /c/Users/decid/Documents/projects/spt-core
echo "=== tests spawning daemon run / endpoint run / rc ==="
grep -rln '"daemon", "run"\|"endpoint", "run"\|"rc",\|run_attach\|endpoint run' crates/spt/tests/ crates/spt-daemon/tests/ 2>/dev/null
echo "=== rc attach in cli (how endpoint run --start vs attach) ==="
grep -n "fn run_attach\|rc_intent\|--once\|RcIntent\|fn cmd_rc" crates/spt/src/rc.rs crates/spt/src/cli.rs 2>/dev/null | head -20

Find real-daemon + rc attach test patterns


=== tests spawning daemon run / endpoint run / rc ===
crates/spt/tests/brain_respawn_rename.rs
crates/spt/tests/brain_restart_psyche_dup_e2e.rs
crates/spt/tests/brain_split.rs
crates/spt/tests/livehost_bootgate_e2e.rs
crates/spt/tests/livehost_bootrace_e2e.rs
crates/spt/tests/livehost_nonresident_e2e.rs
crates/spt/tests/livehost_psyche_fail_e2e.rs
crates/spt/tests/n1_pairing.rs
crates/spt/tests/ready_resume_ledger_e2e.rs
crates/spt/tests/unhost_psyche_reap_e2e.rs
crates/spt-daemon/tests/broker.rs
=== rc attach in cli (how endpoint run --start vs attach) ===
crates/spt/src/rc.rs:182:pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {
crates/spt/src/cli.rs:96:    /// stdout. With --once, runs a single drain+receive cycle and exits.
crates/spt/src/cli.rs:1134:            let intent = rc_intent(view, take);
crates/spt/src/cli.rs:1237:fn rc_intent(view: bool, take: bool) -> spt_net::net::attach::AttachIntent {
crates/spt/src/cli.rs:1333:    match crate::rc::run_attach(id, rc_intent(view, false)) {
crates/spt/src/cli.rs:2959:        // Going offline (the --once listener ends here): drop the perch's
crates/spt/src/cli.rs:4936:    spt ready <id> --once
crates/spt/src/cli.rs:5040:Do NOT use `--once` for a live agent. `--once` drains the backlog and forwards
crates/spt/src/cli.rs:5066:        "receive messages: listener guidance + the --once fallback loop",
crates/spt/src/cli.rs:5081:        "run as a live agent: the persistent listen relay, --once is NOT it, the Psyche seam",
crates/spt/src/cli.rs:7670:        // [unit->REQ-MSG-2] `ready --once` absorbs the removed `poll`'s
crates/spt/src/cli.rs:7673:            parse(&["spt", "ready", "bob", "--once"]).unwrap().cmd.unwrap(),
crates/spt/src/cli.rs:7678:            "poll is removed (ready --once)"
crates/spt/src/cli.rs:10531:            ready.contains("--once"),
crates/spt/src/cli.rs:10564:        // live: the persistent listen relay is the live path; `--once` is
crates/spt/src/cli.rs:10570:            live.contains("Do NOT use `--once`"),
crates/spt/src/cli.rs:10571:            "the --once degenerate path is warned against for live bringup"


`brain_restart_psyche_dup_e2e.rs` (my recent real-daemon test) is the closest template. Read it for the daemon-bringup + hosted-endpoint + reap pattern.


1	//! REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP int — a bare brain restart leaves EXACTLY ONE
2	//! `{id}-psyche` process per endpoint, not a duplicate.
3	//!
4	//! The operator scenario ("brain kill+restart wedged everything"): on an abrupt brain
5	//! death `stop_host` never runs (the LiveSet + owned child handles die with the brain)
6	//! and the Breap job/group only reaps at DAEMON stop — so the PRIOR brain's Psyche
7	//! stays alive. The respawned brain re-hosts and spawns a SECOND Psyche, overwriting
8	//! the `{id}-psyche` perch pid → the old one is now untracked + alive = a DUPLICATE
9	//! that lingers until daemon-stop. The fix sweeps the orphan at brain start (BEFORE
10	//! re-hosting), id-specifically (cmdline carries `<id>-psyche`), so re-hosting yields
11	//! exactly one.
12	//!
13	//! This drives it through the REAL `spt daemon run` broker + supervised brain: host a
14	//! resident Psyche (pid P1), KILL THE BRAIN CHILD with a SINGLE-pid kill (NOT a tree
15	//! kill — P1 must survive, orphaned, to be swept), let the supervisor respawn the
16	//! brain, then assert P1 is DEAD (reaped by the brain-start sweep) and the CURRENT
17	//! `{id}-psyche` pid P2 is a DIFFERENT, ALIVE process — exactly one psyche.
18	//!
19	//! Bounded; all spawned pids reaped scoped (never machine-wide).
20	// [int->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
21
22	use std::path::{Path, PathBuf};
23	use std::process::{Child, Command, Output, Stdio};
24	use std::time::{Duration, Instant};
25
26	mod common;
27	use common::CommandNoWindowExt;
28
29	use spt_store::perch::{self, ParentHint};
30
31	/// Scoped pid-tree kill (cleanup) — never machine-wide.
32	fn kill_pid(pid: u32) {
33	    #[cfg(windows)]
34	    let _ = Command::new("taskkill")
35	        .no_window()
36	        .args(["/PID", &pid.to_string(), "/F", "/T"])
37	        .output();
38	    #[cfg(unix)]
39	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
40	}
41
42	/// Kill ONE process by pid WITHOUT its tree — so the brain dies but its child Psyche
43	/// is orphaned (survives), the exact precondition the brain-start sweep must handle.
44	fn kill_pid_single(pid: u32) {
45	    #[cfg(windows)]
46	    let _ = Command::new("taskkill")
47	        .no_window()
48	        .args(["/PID", &pid.to_string(), "/F"]) // NO /T → no tree kill
49	        .output();
50	    #[cfg(unix)]
51	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
52	}
53
54	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
55	    let (tx, rx) = std::sync::mpsc::channel();
56	    std::thread::spawn(move || {
57	        let _ = tx.send(cmd.output());
58	    });
59	    rx.recv_timeout(deadline)
60	        .expect("captured spt call must complete within its deadline")
61	        .expect("run spt")
62	}
63
64	/// The brain pid in `brain.ready` (self-describing JSON), or None.
65	fn ready_pid(path: &Path) -> Option<u32> {
66	    let s = std::fs::read_to_string(path).ok()?;
67	    serde_json::from_str::<serde_json::Value>(&s)
68	        .ok()?
69	        .get("pid")?
70	        .as_u64()
71	        .map(|p| p as u32)
72	}
73
74	fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
75	    let deadline = Instant::now() + budget;
76	    while Instant::now() < deadline {
77	        if let Some(p) = ready_pid(path) {
78	            return Some(p);
79	        }
80	        std::thread::sleep(Duration::from_millis(50));
81	    }
82	    None
83	}
84
85	#[test]
86	fn brain_restart_leaves_exactly_one_psyche_per_endpoint() {
87	    // ── (1) Isolated, NET-LESS SPT_HOME. ──
88	    let home = tempfile::tempdir().unwrap();
89	    std::env::set_var("SPT_HOME", home.path());
90	    let identity_dir = home.path().join("identity");
91	    std::fs::create_dir_all(&identity_dir).unwrap();
92	    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();
93
94	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
95	    let exe_suffix = spt_bin
96	        .extension()
97	        .map(|e| format!(".{}", e.to_string_lossy()))
98	        .unwrap_or_default();
99
100	    // ── (2) Live-capable adapter; the psyche is LONG-LIVED and bakes {id} into argv
101	    //    (`psychebin ready {id}` → the psyche id "<endpoint>-psyche" appears in its
102	    //    command line, the id-specific reap discriminator). ──
103	    let src = perch::spt_home().join("srcs").join("mocklive");
104	    std::fs::create_dir_all(&src).unwrap();
105	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
106	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
107	    let drops = home.path().join("drops");
108	    std::fs::create_dir_all(&drops).unwrap();
109	    let dir = drops.to_string_lossy().replace('\\', "/");
110	    let manifest_toml = format!(
111	        "[adapter]\nname=\"mocklive\"\nkind=\"harness\"\nversion=\"1\"\n\
112	         min_spt_core_version=\"0\"\n\n\
113	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
114	         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
115	    );
116	    let manifest_path = src.join("manifest.toml");
117	    std::fs::write(&manifest_path, &manifest_toml).unwrap();
118	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
119
120	    // ── (3) Launch the REAL daemon broker PROCESS (spawns the supervised brain). ──
121	    let brain_log = home.path().join("brain.stderr.log");
122	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
123	    let mut broker: Child = Command::new(&spt_bin)
124	        .no_window()
125	        .args(["daemon", "run"])
126	        .env("SPT_HOME", home.path())
127	        .stdout(Stdio::null())
128	        .stderr(Stdio::from(brain_log_file))
129	        .spawn()
130	        .expect("spawn spt daemon run (broker process)");
131
132	    let ready_path = home.path().join("brain.ready");
133	    let brain1 = match wait_for_ready_pid(&ready_path, Duration::from_secs(30)) {
134	        Some(p) => p,
135	        None => {
136	            let _ = broker.kill();
137	            let _ = broker.wait();
138	            panic!(
139	                "PRECONDITION: brain never came up.\n{}",
140	                std::fs::read_to_string(&brain_log).unwrap_or_default()
141	            );
142	        }
143	    };
144
145	    // ── (4) Establish→online: `api seed` + persistent `api listen` → brain hosts P1. ──
146	    let id = "dupagent";
147	    let pid = std::process::id().to_string();
148	    let mp = manifest_path.to_string_lossy().to_string();
149	    let seed = {
150	        let mut cmd = Command::new(&spt_bin);
151	        cmd.no_window()
152	            .args([
153	                "api", "--adapter", "mocklive", "seed", "--pid", &pid, "--session-id", "sid-dup",
154	            ])
155	            .env("SPT_HOME", home.path());
156	        output_bounded(cmd, Duration::from_secs(30))
157	    };
158	    assert!(seed.status.success(), "api seed: {}", String::from_utf8_lossy(&seed.stderr));
159
160	    let listen_log = home.path().join("listen.stderr.log");
161	    let listen_log_file = std::fs::File::create(&listen_log).expect("create listen stderr log");
162	    let mut listen: Child = Command::new(&spt_bin)
163	        .no_window()
164	        .args([
165	            "api", "--adapter", "mocklive", "--manifest", &mp, "listen", id, "--parent-pid", &pid,
166	        ])
167	        .env("SPT_HOME", home.path())
168	        .stdout(Stdio::null())
169	        .stderr(Stdio::from(listen_log_file))
170	        .spawn()
171	        .expect("spawn the persistent api listen");
172
173	    let psyche_perch =
174	        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
175	    let read_psyche_pid = || spt_store::info::read_pid(&psyche_perch);
176
177	    // Wait for the FIRST resident psyche P1.
178	    let p1 = {
179	        let deadline = Instant::now() + Duration::from_secs(30);
180	        let mut got = None;
181	        while Instant::now() < deadline {
182	            if let Some(p) = read_psyche_pid() {
183	                if spt_store::proc::is_process_alive(p) {
184	                    got = Some(p);
185	                    break;
186	                }
187	            }
188	            std::thread::sleep(Duration::from_millis(200));
189	        }
190	        got
191	    };
192	    let p1 = match p1 {
193	        Some(p) => p,
194	        None => {
195	            kill_pid(listen.id());
196	            let _ = listen.wait();
197	            kill_pid(brain1);
198	            let _ = broker.kill();
199	            let _ = broker.wait();
200	            panic!(
201	                "PRECONDITION: no resident P1 psyche.\n=== brain ===\n{}\n=== listen ===\n{}",
202	                std::fs::read_to_string(&brain_log).unwrap_or_default(),
203	                std::fs::read_to_string(&listen_log).unwrap_or_default()
204	            );
205	        }
206	    };
207
208	    // ── (5) KILL THE BRAIN CHILD (single-pid, NO tree) → P1 orphaned + alive. The
209	    //    supervisor respawns a NEW brain (different pid). ──
210	    kill_pid_single(brain1);
211	    let brain2 = {
212	        let deadline = Instant::now() + Duration::from_secs(40);
213	        let mut got = None;
214	        while Instant::now() < deadline {
215	            if let Some(p) = ready_pid(&ready_path) {
216	                if p != brain1 && spt_store::proc::is_process_alive(p) {
217	                    got = Some(p);
218	                    break;
219	                }
220	            }
221	            std::thread::sleep(Duration::from_millis(100));
222	        }
223	        got
224	    };
225	    let brain2 = match brain2 {
226	        Some(p) => p,
227	        None => {
228	            kill_pid(p1);
229	            kill_pid(listen.id());
230	            let _ = listen.wait();
231	            let _ = broker.kill();
232	            let _ = broker.wait();
233	            panic!(
234	                "PRECONDITION: supervisor never respawned a NEW brain (still {brain1}).\n{}",
235	                std::fs::read_to_string(&brain_log).unwrap_or_default()
236	            );
237	        }
238	    };
239
240	    // ── (6) THE ASSERTION: the brain-start sweep reaped P1, and the re-host produced a
241	    //    NEW psyche P2 — exactly ONE alive. Poll: P1 dead AND a DIFFERENT alive pid on
242	    //    the perch. ──
243	    let mut p1_dead = false;
244	    let mut p2: Option<u32> = None;
245	    let deadline = Instant::now() + Duration::from_secs(40);
246	    while Instant::now() < deadline {
247	        if !spt_store::proc::is_process_alive(p1) {
248	            p1_dead = true;
249	        }
250	        if let Some(p) = read_psyche_pid() {
251	            if p != p1 && spt_store::proc::is_process_alive(p) {
252	                p2 = Some(p);
253	            }
254	        }
255	        if p1_dead && p2.is_some() {
256	            break;
257	        }
258	        std::thread::sleep(Duration::from_millis(200));
259	    }
260
261	    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
262	    eprintln!("=== DIAGNOSTIC: p1={p1} p1_dead={p1_dead} p2={p2:?} brain1={brain1} brain2={brain2} ===");
263	    eprintln!("=== brain stderr (BRAIN_RESTART_PSYCHE_REAP lands here) ===\n{brain_stderr}");
264
265	    // ── (7) Reap scoped. ──
266	    if !p1_dead {
267	        kill_pid(p1);
268	    }
269	    if let Some(p) = p2 {
270	        kill_pid(p);
271	    }
272	    kill_pid(listen.id());
273	    let _ = listen.wait();
274	    kill_pid(brain2);
275	    kill_pid(brain1);
276	    let _ = broker.kill();
277	    let _ = broker.wait();
278
279	    assert!(
280	        p1_dead,
281	        "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP: the PRIOR brain's psyche P1={p1} must be REAPED by \
282	         the respawned brain's start sweep (it lingered = the duplicate hazard).\n\
283	         === brain stderr ===\n{brain_stderr}"
284	    );
285	    assert!(
286	        p2.is_some(),
287	        "the respawned brain must re-host a NEW psyche (exactly one, not zero) — none appeared.\n\
288	         === brain stderr ===\n{brain_stderr}"
289	    );
290	}
291

//! REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP int — a bare brain restart leaves EXACTLY ONE
//! `{id}-psyche` process per endpoint, not a duplicate.
//!
//! The operator scenario ("brain kill+restart wedged everything"): on an abrupt brain
//! death `stop_host` never runs (the LiveSet + owned child handles die with the brain)
//! and the Breap job/group only reaps at DAEMON stop — so the PRIOR brain's Psyche
//! stays alive. The respawned brain re-hosts and spawns a SECOND Psyche, overwriting
//! the `{id}-psyche` perch pid → the old one is now untracked + alive = a DUPLICATE
//! that lingers until daemon-stop. The fix sweeps the orphan at brain start (BEFORE
//! re-hosting), id-specifically (cmdline carries `<id>-psyche`), so re-hosting yields
//! exactly one.
//!
//! This drives it through the REAL `spt daemon run` broker + supervised brain: host a
//! resident Psyche (pid P1), KILL THE BRAIN CHILD with a SINGLE-pid kill (NOT a tree
//! kill — P1 must survive, orphaned, to be swept), let the supervisor respawn the
//! brain, then assert P1 is DEAD (reaped by the brain-start sweep) and the CURRENT
//! `{id}-psyche` pid P2 is a DIFFERENT, ALIVE process — exactly one psyche.
//!
//! Bounded; all spawned pids reaped scoped (never machine-wide).
// [int->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]

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

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

/// Kill ONE process by pid WITHOUT its tree — so the brain dies but its child Psyche
/// is orphaned (survives), the exact precondition the brain-start sweep must handle.
fn kill_pid_single(pid: u32) {
    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .no_window()
        .args(["/PID", &pid.to_string(), "/F"]) // NO /T → no tree kill
        .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")
}

/// The brain pid in `brain.ready` (self-describing JSON), or None.
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
}

#[test]
fn brain_restart_leaves_exactly_one_psyche_per_endpoint() {
    // ── (1) Isolated, NET-LESS SPT_HOME. ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    let identity_dir = home.path().join("identity");
    std::fs::create_dir_all(&identity_dir).unwrap();
    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();

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

    // ── (2) Live-capable adapter; the psyche is LONG-LIVED and bakes {id} into argv
    //    (`psychebin ready {id}` → the psyche id "<endpoint>-psyche" appears in its
    //    command line, the id-specific reap discriminator). ──
    let src = perch::spt_home().join("srcs").join("mocklive");
    std::fs::create_dir_all(&src).unwrap();
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"mocklive\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    let manifest_path = src.join("manifest.toml");
    std::fs::write(&manifest_path, &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // ── (3) Launch the REAL daemon broker PROCESS (spawns the supervised brain). ──
    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 ready_path = home.path().join("brain.ready");
    let brain1 = match wait_for_ready_pid(&ready_path, 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) Establish→online: `api seed` + persistent `api listen` → brain hosts P1. ──
    let id = "dupagent";
    let pid = std::process::id().to_string();
    let mp = manifest_path.to_string_lossy().to_string();
    let seed = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args([
                "api", "--adapter", "mocklive", "seed", "--pid", &pid, "--session-id", "sid-dup",
            ])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(30))
    };
    assert!(seed.status.success(), "api seed: {}", String::from_utf8_lossy(&seed.stderr));

    let listen_log = home.path().join("listen.stderr.log");
    let listen_log_file = std::fs::File::create(&listen_log).expect("create listen stderr log");
    let mut listen: Child = Command::new(&spt_bin)
        .no_window()
        .args([
            "api", "--adapter", "mocklive", "--manifest", &mp, "listen", id, "--parent-pid", &pid,
        ])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(listen_log_file))
        .spawn()
        .expect("spawn the persistent api listen");

    let psyche_perch =
        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    let read_psyche_pid = || spt_store::info::read_pid(&psyche_perch);

    // Wait for the FIRST resident psyche P1.
    let p1 = {
        let deadline = Instant::now() + Duration::from_secs(30);
        let mut got = None;
        while Instant::now() < deadline {
            if let Some(p) = read_psyche_pid() {
                if spt_store::proc::is_process_alive(p) {
                    got = Some(p);
                    break;
                }
            }
            std::thread::sleep(Duration::from_millis(200));
        }
        got
    };
    let p1 = match p1 {
        Some(p) => p,
        None => {
            kill_pid(listen.id());
            let _ = listen.wait();
            kill_pid(brain1);
            let _ = broker.kill();
            let _ = broker.wait();
            panic!(
                "PRECONDITION: no resident P1 psyche.\n=== brain ===\n{}\n=== listen ===\n{}",
                std::fs::read_to_string(&brain_log).unwrap_or_default(),
                std::fs::read_to_string(&listen_log).unwrap_or_default()
            );
        }
    };

    // ── (5) KILL THE BRAIN CHILD (single-pid, NO tree) → P1 orphaned + alive. The
    //    supervisor respawns a NEW brain (different pid). ──
    kill_pid_single(brain1);
    let brain2 = {
        let deadline = Instant::now() + Duration::from_secs(40);
        let mut got = None;
        while Instant::now() < deadline {
            if let Some(p) = ready_pid(&ready_path) {
                if p != brain1 && spt_store::proc::is_process_alive(p) {
                    got = Some(p);
                    break;
                }
            }
            std::thread::sleep(Duration::from_millis(100));
        }
        got
    };
    let brain2 = match brain2 {
        Some(p) => p,
        None => {
            kill_pid(p1);
            kill_pid(listen.id());
            let _ = listen.wait();
            let _ = broker.kill();
            let _ = broker.wait();
            panic!(
                "PRECONDITION: supervisor never respawned a NEW brain (still {brain1}).\n{}",
                std::fs::read_to_string(&brain_log).unwrap_or_default()
            );
        }
    };

    // ── (6) THE ASSERTION: the brain-start sweep reaped P1, and the re-host produced a
    //    NEW psyche P2 — exactly ONE alive. Poll: P1 dead AND a DIFFERENT alive pid on
    //    the perch. ──
    let mut p1_dead = false;
    let mut p2: Option<u32> = None;
    let deadline = Instant::now() + Duration::from_secs(40);
    while Instant::now() < deadline {
        if !spt_store::proc::is_process_alive(p1) {
            p1_dead = true;
        }
        if let Some(p) = read_psyche_pid() {
            if p != p1 && spt_store::proc::is_process_alive(p) {
                p2 = Some(p);
            }
        }
        if p1_dead && p2.is_some() {
            break;
        }
        std::thread::sleep(Duration::from_millis(200));
    }

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!("=== DIAGNOSTIC: p1={p1} p1_dead={p1_dead} p2={p2:?} brain1={brain1} brain2={brain2} ===");
    eprintln!("=== brain stderr (BRAIN_RESTART_PSYCHE_REAP lands here) ===\n{brain_stderr}");

    // ── (7) Reap scoped. ──
    if !p1_dead {
        kill_pid(p1);
    }
    if let Some(p) = p2 {
        kill_pid(p);
    }
    kill_pid(listen.id());
    let _ = listen.wait();
    kill_pid(brain2);
    kill_pid(brain1);
    let _ = broker.kill();
    let _ = broker.wait();

    assert!(
        p1_dead,
        "REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP: the PRIOR brain's psyche P1={p1} must be REAPED by \
         the respawned brain's start sweep (it lingered = the duplicate hazard).\n\
         === brain stderr ===\n{brain_stderr}"
    );
    assert!(
        p2.is_some(),
        "the respawned brain must re-host a NEW psyche (exactly one, not zero) — none appeared.\n\
         === brain stderr ===\n{brain_stderr}"
    );
}


Template clear. The v0.12.1 defects are in the **spt-hosted** path (`endpoint run`→broker PTY), not the harness-hosted `api listen` path. Wave 1 fixture must drive `endpoint run`. Confirm its exact CLI arg shape before writing the test.


cd /c/Users/decid/Documents/projects/spt-core
sed -n '250,275p' crates/spt/src/cli.rs
echo "=== EndpointCmd::Run / Run variant ==="
grep -n "Run {" crates/spt/src/cli.rs | head

endpoint run clap arg shape


    /// views per the terminal-action flag. The endpoint id rides argv so the
    /// harness binds to exactly it. This is the non-interactive core (the
    /// interactive picker lands in a later wave); the flags cover every terminal
    /// action so a `spt-<id>` shortcut can bake a fully non-interactive launch.
    Run {
        /// The harness adapter to host: `<adapter>[:profile]` (must be a
        /// registered `kind="harness"` adapter on this node). Omit (with --id)
        /// to launch the interactive picker.
        #[arg(long)]
        adapter: Option<String>,
        /// The endpoint id to bring up (charset: alphanumeric, `-`, `_`). Omit
        /// (with --adapter) to launch the interactive picker.
        #[arg(long)]
        id: Option<String>,
        /// Mint a fresh session (the default; explicit so a non-interactive
        /// shortcut can bake create-vs-resume). Conflicts with --resume.
        #[arg(long, conflicts_with = "resume")]
        create: bool,
        /// Resume a prior session id instead of minting a fresh one.
        #[arg(long)]
        resume: Option<String>,
        /// Start the endpoint and return immediately (no attach).
        #[arg(long, conflicts_with_all = ["attach", "view"])]
        start: bool,
        /// Attach a local terminal after bringup (the default action).
        #[arg(long, conflicts_with_all = ["start", "view"])]
=== EndpointCmd::Run / Run variant ===
254:    Run {
1107:            EndpointCmd::Run {


`spt endpoint run --adapter A --id X --start`. Now check how contract_e2e resolves the `mock-session` bin path (sibling_bin) — I need it for the dummy `[session.self]`.


cd /c/Users/decid/Documents/projects/spt-core
grep -n "fn sibling_bin" crates/spt/tests/contract_e2e.rs
sed -n "/fn sibling_bin/,/^    }/p" crates/spt/tests/contract_e2e.rs | head -30

Read sibling_bin helper


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

fn keys(pairs: &[(&str, &str)]) -> std::collections::BTreeMap<String, String> {
    pairs
        .iter()
        .map(|(k, v)| (k.to_string(), v.to_string()))
        .collect()
}

/// Stand up an **in-process** seed daemon on the current `SPT_HOME`'s seed
/// socket so the child `spt api …` invocations find a running daemon (their
/// `ensure_running` ping succeeds → no real `spt daemon` spawned). The serve
/// thread is leaked; it dies with the test process. Call after `SPT_HOME` is set.
// [int->REQ-DAEMON-3]
fn start_inproc_daemon() {
    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
    let name = spt_daemon::seed_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);
    });


`mock-session.exe` lands beside `spt.exe` in the target dir. Writing the Wave 1 fixture smoke test: real daemon → `endpoint run --start` dummy → assert online + `rc` attach sees `DUMMY_HARNESS_TICK`.


Write

//! 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, NET-LESS SPT_HOME. ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    let identity_dir = home.path().join("identity");
    std::fs::create_dir_all(&identity_dir).unwrap();
    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let 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"
    );
    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"
    );
    let src = perch::spt_home().join("srcs").join("dummyharness");
    std::fs::create_dir_all(&src).unwrap();
    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 harness pid the bind stamped — it must stay alive (heartbeating), proving
    // a long-lived PTY child (not the immediate-exit mock).
    let harness_pid = spt_store::info::read_pid(&self_perch);
    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 mut rc: Child = Command::new(&spt_bin)
        .no_window()
        .args(["rc", id])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .stdin(Stdio::null())
        .spawn()
        .expect("spawn spt rc <id>");
    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_saw_tick = rx
        .recv_timeout(Duration::from_secs(10))
        .map(|s| s.contains("DUMMY_HARNESS_TICK"))
        .unwrap_or(false);

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!(
        "=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \
         alive_after={alive_after} rc_saw_tick={rc_saw_tick} ===\n=== endpoint run stderr ===\n{}",
        run.status.success(),
        String::from_utf8_lossy(&run.stderr),
    );

    // ── (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);
    }
    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:?}"
    );
    assert!(
        rc_saw_tick,
        "`spt rc <id>` must attach and receive the live DUMMY_HARNESS_TICK heartbeat \
         (the real broker-PTY → rc attach path).\n=== brain stderr ===\n{brain_stderr}"
    );
}


<owl_messages>
<owl_message from="doyle" priority="high">Real-harness repro data from clearing the operator's wedged machine (use it for the Wave 2 gates):

1. UNHOST-PSYCHE-REAP fails in the REAL harness too (v0.12.0's reap didn't land): a DEAD endpoint (wall-a, harness process gone) left its `{id}-psyche` HARNESS PROCESS (claude-spt-psyche.exe --id wall-a-psyche) ALIVE + a `spt ready wall-a-psyche --once` alive. The orphan psyche held a file in the perch tree, so `spt endpoint purge wall-a --force` STOPPED fine but FAILED to remove the tree: "os error 32: The process cannot access the file because it is being used by another process." Only after I scope-killed the orphan psyche did purge succeed. So your dummy-harness E2E should assert: when an spt-hosted endpoint's harness DIES (or is stopped), its {id}-psyche process is REAPED (not left holding perch files). Fold into L2/L4.

2. Control plane vs data plane confirmed: `endpoint purge --force` STOP worked instantly on all three (control plane responsive); only attach/output (data plane, the 2-worker loopback) was wedged. Matches the ATTACH-WEDGE root.

3. ensure_running respawns a fresh daemon the moment a CLI command finds none (killing the daemon → next CLI call brings up a new one). Account for that in any daemon-stop E2E: assert stop leaves it down for a bounded window WITHOUT a CLI call racing a respawn.

Not blocking your Wave 1 — just real data to make the gates faithful.</owl_message>
</owl_messages>

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


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

//! 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, NET-LESS SPT_HOME. ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    let identity_dir = home.path().join("identity");
    std::fs::create_dir_all(&identity_dir).unwrap();
    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let 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"
    );
    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"
    );
    let src = perch::spt_home().join("srcs").join("dummyharness");
    std::fs::create_dir_all(&src).unwrap();
    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 harness pid the bind stamped — it must stay alive (heartbeating), proving
    // a long-lived PTY child (not the immediate-exit mock).
    let harness_pid = spt_store::info::read_pid(&self_perch);
    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 mut rc: Child = Command::new(&spt_bin)
        .no_window()
        .args(["rc", id])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .stdin(Stdio::null())
        .spawn()
        .expect("spawn spt rc <id>");
    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_saw_tick = rx
        .recv_timeout(Duration::from_secs(10))
        .map(|s| s.contains("DUMMY_HARNESS_TICK"))
        .unwrap_or(false);

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!(
        "=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \
         alive_after={alive_after} rc_saw_tick={rc_saw_tick} ===\n=== endpoint run stderr ===\n{}",
        run.status.success(),
        String::from_utf8_lossy(&run.stderr),
    );

    // ── (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);
    }
    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:?}"
    );
    assert!(
        rc_saw_tick,
        "`spt rc <id>` must attach and receive the live DUMMY_HARNESS_TICK heartbeat \
         (the real broker-PTY → rc attach path).\n=== brain stderr ===\n{brain_stderr}"
    );
}


doyle's real-harness repro data (ACK — folds into Wave 2 gates, not Wave 1):
1. UNHOST-PSYCHE-REAP fails in real harness too — dead endpoint's `{id}-psyche` survives + holds perch files → purge os-error-32. Gate must assert harness-death reaps the psyche.
2. Control-plane responsive, only data-plane (2-worker loopback) wedged → confirms ATTACH-WEDGE root.
3. `ensure_running` respawns daemon on any CLI call finding none → daemon-stop gate must assert down-for-a-bounded-window without a racing CLI call.

ACK + build/run Wave 1 (pre-build mock-session, then the fixture test).


Bash

cd /c/Users/decid/Documents/projects/spt-core
$OWL send doyle <<'EOF'
ACK real-harness repro data — folding into Wave 2: (L2/L4) gate asserts a DIED/stopped spt-hosted harness gets its {id}-psyche REAPED (no orphan holding perch files → no purge os-error-32); (3) daemon-stop gate asserts down-for-bounded-window with NO racing CLI call (ensure_running respawns on any CLI find-none); (2) control-vs-data plane confirms ATTACH-WEDGE = 2-worker loopback. Wave 1 dummy adapter has no psyche_init (no psyche) so psyche-reap is a Wave 2 concern. Building+running Wave 1 fixture now.
EOF
echo "=== pre-build mock-session ==="
CARGO_TARGET_DIR=target-seam cargo build -p mock-adapter --bin mock-session 2>&1 | tail -4
echo "=== run Wave 1 dummy_harness_e2e ==="
CARGO_TARGET_DIR=target-seam cargo test -p spt --test dummy_harness_e2e -- --test-threads=1 --nocapture 2>&1 | grep -E "DIAGNOSTIC|test result:|panicked|^test .* \.\.\. (ok|FAILED)|error\[|error:" | head -40
echo "RUN_EXIT=${PIPESTATUS[0]}"

ACK doyle + build mock-session + run Wave 1 fixture


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


Wave 1 build+run in flight (real daemon + PTY). Awaiting result.


1	➜ SENT:doyle
2	=== pre-build mock-session ===
3	   Compiling mock-adapter v0.12.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
4	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
5	=== run Wave 1 dummy_harness_e2e ===
6

➜ SENT:doyle
=== pre-build mock-session ===
   Compiling mock-adapter v0.12.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
=== run Wave 1 dummy_harness_e2e ===


Compiling the test binary. Wait for run completion.


ack wait


awaiting bjbegrr4a Wave 1 result


Awaiting Wave 1 result (real-daemon PTY test compiles + runs). Will be notified on completion.


<task-notification>
<task-id>bjbegrr4a</task-id>
<tool-use-id>toolu_01HatE8wMPcHXMcUCeabRnLo</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bjbegrr4a.output</output-file>
<status>completed</status>
<summary>Background command "ACK doyle + build mock-session + run Wave 1 fixture" completed (exit code 0)</summary>
</task-notification>


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


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


1	➜ SENT:doyle
2	=== pre-build mock-session ===
3	   Compiling mock-adapter v0.12.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
4	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
5	=== run Wave 1 dummy_harness_e2e ===
6	test endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches ... === DIAGNOSTIC: run_ok=true online=false harness_pid=Some(238836) alive_after=false rc_saw_tick=false ===
7	thread 'endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches' (246076) panicked at crates\spt\tests\dummy_harness_e2e.rs:232:5:
8	test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 23.37s
9	error: test failed, to rerun pass `-p spt --test dummy_harness_e2e`
10	RUN_EXIT=101
11

➜ SENT:doyle
=== pre-build mock-session ===
   Compiling mock-adapter v0.12.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
=== run Wave 1 dummy_harness_e2e ===
test endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches ... === DIAGNOSTIC: run_ok=true online=false harness_pid=Some(238836) alive_after=false rc_saw_tick=false ===
thread 'endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches' (246076) panicked at crates\spt\tests\dummy_harness_e2e.rs:232:5:
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 23.37s
error: test failed, to rerun pass `-p spt --test dummy_harness_e2e`
RUN_EXIT=101


Wave 1 FAILED: `run_ok=true` but `online=false`, `harness_pid=Some(238836)` dead, `rc_saw_tick=false`. The dummy bound a pid then died — likely its `spt api bind` failed. Need the real error + the working bind contract. Read how `contract_e2e`'s mock drives the spt-hosted bind (the working reference).