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

//! Daemon child reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the daemon's
//! spt-hosted subtree — the supervised brain child and everything it spawns
//! (the shellwake watchers, the DETACHED live-agent Psyches) — must die WITH the
//! daemon on `spt daemon stop`, never linger as orphaned `claude-spt-psyche.exe`
//! / `spt.exe`. Today a stop orphans ~8 of them: the Psyches are spawned
//! detached (`spt_runtime::runtime` drops the `Child`) and the brain's livehost /
//! shellwake stop flags are held "for symmetry", never raised — so killing the
//! daemon leaves the whole brain subtree running.
//!
//! The fix reaps the brain subtree as ONE OS unit rooted at the brain — never at
//! the daemon — so a reap tears down the children without self-terminating the
//! daemon (it reaps, then returns from `Daemon::run` cleanly):
//!   * Windows: a Job object the brain is assigned to (its descendants inherit
//!     the job at creation — no per-grandchild bookkeeping, no assignment race
//!     for Psyches the brain spawns much later). `KILL_ON_JOB_CLOSE` is the
//!     crash backstop (a daemon that dies without a graceful stop still drops the
//!     last job handle at process exit → the OS reaps the members); the graceful
//!     path calls [`BrainReaper::reap`] for a deterministic, immediate kill.
//!   * Unix: a process group the brain leads (it `setpgid(0, 0)`s itself at spawn
//!     via `pre_exec`); its descendants inherit the group. The graceful path
//!     `kill(-pgid, SIGKILL)`s the group — the daemon is in its OWN group, so it
//!     is never signalled.
//!
//! Pairs with the B3 stop barrier and the supervisor stop flag the daemon raises
//! before reaping (so the supervisor does not respawn the brain being killed).
// [impl->REQ-HAZARD-DAEMON-STOP-REAP]

use std::process::Child;
use std::sync::Arc;

/// The reap container for the daemon's brain subtree. Created once at daemon
/// start; the supervisor [`enroll`](BrainReaper::enroll)s every (re)spawned brain
/// child, and the graceful-stop path [`reap`](BrainReaper::reap)s the subtree.
///
/// Send + Sync (a raw Job handle is a thread-safe integer; the Unix pgid is an
/// atomic) so the daemon foreground and the supervisor thread share one `Arc`.
pub struct BrainReaper {
    /// The Win32 Job object handle (`isize`), or `0` when the platform refused to
    /// create/assign one — a no-op reaper, never a failed daemon start.
    #[cfg(windows)]
    job: isize,
    /// The current brain's process-group id (its pid, since it leads its own
    /// group), or `0` before the first enroll. A respawn overwrites it — only the
    /// current brain + its descendants exist to reap.
    #[cfg(unix)]
    pgid: std::sync::atomic::AtomicI32,
}

impl BrainReaper {
    /// Create the reap container. Best-effort on Windows: a platform that refuses
    /// the Job object (e.g. already inside a job that forbids nesting) yields a
    /// no-op reaper rather than aborting daemon start — orphan-reaping is a
    /// hardening, never a prerequisite to serve.
    pub fn new() -> Arc<Self> {
        #[cfg(windows)]
        {
            Arc::new(BrainReaper {
                job: windows_impl::create_kill_on_close_job(),
            })
        }
        #[cfg(unix)]
        {
            Arc::new(BrainReaper {
                pgid: std::sync::atomic::AtomicI32::new(0),
            })
        }
    }

    /// Enroll a freshly-spawned brain `child` so it AND its descendants reap as
    /// one unit. Windows: assign the child to the Job (its later-spawned Psyches
    /// inherit it). Unix: record the child's pid as the group id (the child set
    /// its own group via `pre_exec` at spawn — see `spawn_brain_child`).
    pub fn enroll(&self, child: &Child) {
        #[cfg(windows)]
        {
            if self.job != 0 {
                windows_impl::assign_to_job(self.job, child);
            }
        }
        #[cfg(unix)]
        {
            self.pgid
                .store(child.id() as i32, std::sync::atomic::Ordering::SeqCst);
        }
    }

    /// Reap the whole brain subtree NOW — the graceful `daemon stop` path.
    /// Windows: `TerminateJobObject` (the daemon is not in the job → unaffected).
    /// Unix: `kill(-pgid, SIGKILL)` of the brain's group (the daemon is in its own
    /// group → unaffected). A no-op when nothing was ever enrolled.
    pub fn reap(&self) {
        #[cfg(windows)]
        {
            if self.job != 0 {
                windows_impl::terminate_job(self.job);
            }
        }
        #[cfg(unix)]
        {
            let pgid = self.pgid.load(std::sync::atomic::Ordering::SeqCst);
            if pgid > 0 {
                // Negative pid → "the process group". SIGKILL is deterministic:
                // the orphan hazard is about leftover processes, so guarantee the
                // reap rather than rely on a catchable SIGTERM the harness child
                // might ignore.
                unsafe {
                    libc::kill(-pgid, libc::SIGKILL);
                }
            }
        }
    }
}

#[cfg(windows)]
mod windows_impl {
    use std::ffi::c_void;
    use std::os::windows::io::AsRawHandle;
    use std::process::Child;

    // JOBOBJECT_EXTENDED_LIMIT_INFORMATION class + the kill-on-close limit flag.
    const JOB_OBJECT_EXTENDED_LIMIT_INFORMATION: u32 = 9;
    const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: u32 = 0x0000_2000;

    #[repr(C)]
    #[derive(Default)]
    struct JobObjectBasicLimitInformation {
        per_process_user_time_limit: i64,
        per_job_user_time_limit: i64,
        limit_flags: u32,
        minimum_working_set_size: usize,
        maximum_working_set_size: usize,
        active_process_limit: u32,
        affinity: usize,
        priority_class: u32,
        scheduling_class: u32,
    }

    #[repr(C)]
    #[derive(Default)]
    struct IoCounters {
        read_operation_count: u64,
        write_operation_count: u64,
        other_operation_count: u64,
        read_transfer_count: u64,
        write_transfer_count: u64,
        other_transfer_count: u64,
    }

    #[repr(C)]
    #[derive(Default)]
    struct JobObjectExtendedLimitInformation {
        basic_limit_information: JobObjectBasicLimitInformation,
        io_info: IoCounters,
        process_memory_limit: usize,
        job_memory_limit: usize,
        peak_process_memory_used: usize,
        peak_job_memory_used: usize,
    }

    extern "system" {
        fn CreateJobObjectW(lp_job_attributes: *mut c_void, lp_name: *const u16) -> isize;
        fn SetInformationJobObject(
            h_job: isize,
            job_object_information_class: u32,
            lp_job_object_information: *const c_void,
            cb_job_object_information_length: u32,
        ) -> i32;
        fn AssignProcessToJobObject(h_job: isize, h_process: isize) -> i32;
        fn TerminateJobObject(h_job: isize, u_exit_code: u32) -> i32;
    }

    /// Create an unnamed Job object with `KILL_ON_JOB_CLOSE` set, so a daemon that
    /// dies WITHOUT a graceful stop still reaps the members when the OS closes its
    /// last handle at process exit. Returns `0` on any failure (no-op reaper).
    pub fn create_kill_on_close_job() -> isize {
        let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
        if job == 0 {
            eprintln!("REAP_JOB_CREATE_FAIL: brain subtree will not auto-reap on stop");
            return 0;
        }
        let mut info = JobObjectExtendedLimitInformation::default();
        info.basic_limit_information.limit_flags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
        let ok = unsafe {
            SetInformationJobObject(
                job,
                JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
                &info as *const _ as *const c_void,
                std::mem::size_of::<JobObjectExtendedLimitInformation>() as u32,
            )
        };
        if ok == 0 {
            eprintln!("REAP_JOB_LIMIT_FAIL: kill-on-close not set; brain subtree may orphan");
        }
        job
    }

    /// Assign `child` to `job`. Its descendants inherit the job at creation.
    pub fn assign_to_job(job: isize, child: &Child) {
        let handle = child.as_raw_handle() as isize;
        let ok = unsafe { AssignProcessToJobObject(job, handle) };
        if ok == 0 {
            eprintln!("REAP_JOB_ASSIGN_FAIL: brain not enrolled; its subtree may orphan on stop");
        }
    }

    /// Terminate every process in `job` immediately.
    pub fn terminate_job(job: isize) {
        unsafe {
            TerminateJobObject(job, 1);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use spt_store::proc::is_process_alive;
    use std::process::Command;
    use std::time::Duration;

    /// Poll until `pid` reads dead (zombie counts as dead — `is_process_alive`
    /// handles it) or the bound elapses.
    fn wait_dead(pid: u32) -> bool {
        for _ in 0..250 {
            if !is_process_alive(pid) {
                return true;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        false
    }

    // [unit->REQ-HAZARD-DAEMON-STOP-REAP] an enrolled child is terminated by the
    // reap — the mechanism that makes the brain (and the Psyches it spawns, which
    // inherit the same job/group) die with the daemon on stop instead of
    // orphaning. The child is a long-lived sleeper rooted in its own container;
    // reap() must kill it.
    #[cfg(windows)]
    #[test]
    fn job_reaps_enrolled_child() {
        let reaper = BrainReaper::new();
        // A real long-running child (ping's -n is a 1s/iteration sleeper).
        let mut child = Command::new("cmd")
            .args(["/C", "ping -n 60 127.0.0.1 >NUL"])
            .spawn()
            .expect("spawn sleeper");
        reaper.enroll(&child);
        let pid = child.id();
        assert!(is_process_alive(pid), "enrolled child starts alive");
        reaper.reap();
        assert!(wait_dead(pid), "reap must terminate the enrolled child");
        let _ = child.wait();
    }

    #[cfg(unix)]
    #[test]
    fn group_reaps_enrolled_child() {
        use std::os::unix::process::CommandExt;
        let reaper = BrainReaper::new();
        let mut cmd = Command::new("sh");
        cmd.args(["-c", "sleep 60"]);
        // Mirror the production brain spawn: the child leads its OWN group so the
        // reap (kill -pgid) hits the child subtree, never the test runner.
        unsafe {
            cmd.pre_exec(|| {
                libc::setpgid(0, 0);
                Ok(())
            });
        }
        let mut child = cmd.spawn().expect("spawn sleeper");
        reaper.enroll(&child);
        let pid = child.id();
        assert!(is_process_alive(pid), "enrolled child starts alive");
        reaper.reap();
        assert!(wait_dead(pid), "reap must terminate the enrolled child's group");
        let _ = child.wait();
    }

    // [unit->REQ-HAZARD-DAEMON-STOP-REAP] reap before any enroll is a clean no-op
    // (a daemon stopped before the brain ever spawned must not panic).
    #[test]
    fn reap_without_enroll_is_noop() {
        let reaper = BrainReaper::new();
        reaper.reap();
    }
}


Register the module:


Edit

pub mod pump;
pub mod reap;
pub mod reconcile;


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


139	pub mod psyrelay;
140	pub mod pump;
141	pub mod reconcile;
142	pub mod registryhost;

pub mod psyrelay;
pub mod pump;
pub mod reconcile;
pub mod registryhost;


pub mod pump;
pub mod reap;
pub mod reconcile;


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

pub mod pump;
pub mod reap;
pub mod reconcile;

pub mod propagate;
pub mod psyrelay;
pub mod pump;
+pub mod reap;
pub mod reconcile;
pub mod registryhost;
pub mod relay;


Now B3 — the barrier in `seedmap::request_stop` + its unit. Update `request_stop`:


/// Ask the daemon serving `name` to stop (graceful: it acks `stopping`, then
/// its serve loop returns — `spt daemon stop`, M8 decision 5 / REQ-CLI-2).
// [impl->REQ-CLI-2]
pub fn request_stop(name: &str) -> io::Result<()> {
    let mut conn = connect(name)?;
    write_frame(
        &mut conn,
        &Envelope::new(KIND_STOP, serde_json::Value::Null),
    )?;
    let env = read_frame(&mut conn)?;
    if env.kind == KIND_STOPPING {
        Ok(())
    } else {
        Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "expected stopping ack",
        ))
    }
}

/// How long [`request_stop`] waits for the seed socket to actually go away after
/// the `stopping` ack before giving up. The serve loop returns within one accept
/// of the stop latch (the throwaway dial wakes it), so this only ever spends a
/// few poll slices — the ceiling is a wedge guard, not the expected cost.
const STOP_BARRIER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
/// Poll slice for the stop barrier.
const STOP_BARRIER_POLL: std::time::Duration = std::time::Duration::from_millis(10);

/// Ask the daemon serving `name` to stop (graceful: it acks `stopping`, then
/// its serve loop returns — `spt daemon stop`, M8 decision 5 / REQ-CLI-2).
///
/// Returns only once the daemon has actually stopped answering — the **stop
/// barrier** (REQ-HAZARD-DAEMON-STOP-BARRIER / B3). The `stopping` ack is sent
/// BEFORE the serve loop drops its listener, so returning on the bare ack leaves
/// a window where the socket is still bound: a following `is_running` ping (the
/// `daemon start` liveness check) wins that window and start falsely reports
/// ALREADY_RUNNING. We close it by polling [`ping`] to failure after the ack —
/// when the probe can no longer reach a listener, the stop has fully completed
/// and an immediate restart is safe.
// [impl->REQ-CLI-2]
// [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]
pub fn request_stop(name: &str) -> io::Result<()> {
    let mut conn = connect(name)?;
    write_frame(
        &mut conn,
        &Envelope::new(KIND_STOP, serde_json::Value::Null),
    )?;
    let env = read_frame(&mut conn)?;
    if env.kind != KIND_STOPPING {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "expected stopping ack",
        ));
    }
    drop(conn); // release our connection so it never props the loop open
    // Barrier: the ack is not the stop — wait until the listener is truly gone.
    let mut waited = std::time::Duration::ZERO;
    while waited < STOP_BARRIER_TIMEOUT {
        if ping(name).is_err() {
            return Ok(()); // no listener answers — stop has fully completed
        }
        std::thread::sleep(STOP_BARRIER_POLL);
        waited += STOP_BARRIER_POLL;
    }
    Err(io::Error::new(
        io::ErrorKind::TimedOut,
        "daemon acked stopping but kept answering past the stop barrier",
    ))
}


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

/// Ask the daemon serving `name` to stop (graceful: it acks `stopping`, then
/// its serve loop returns — `spt daemon stop`, M8 decision 5 / REQ-CLI-2).
// [impl->REQ-CLI-2]
pub fn request_stop(name: &str) -> io::Result<()> {
    let mut conn = connect(name)?;
    write_frame(
        &mut conn,
        &Envelope::new(KIND_STOP, serde_json::Value::Null),
    )?;
    let env = read_frame(&mut conn)?;
    if env.kind == KIND_STOPPING {
        Ok(())
    } else {
        Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "expected stopping ack",
        ))
    }
}

/// How long [`request_stop`] waits for the seed socket to actually go away after
/// the `stopping` ack before giving up. The serve loop returns within one accept
/// of the stop latch (the throwaway dial wakes it), so this only ever spends a
/// few poll slices — the ceiling is a wedge guard, not the expected cost.
const STOP_BARRIER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
/// Poll slice for the stop barrier.
const STOP_BARRIER_POLL: std::time::Duration = std::time::Duration::from_millis(10);

/// Ask the daemon serving `name` to stop (graceful: it acks `stopping`, then
/// its serve loop returns — `spt daemon stop`, M8 decision 5 / REQ-CLI-2).
///
/// Returns only once the daemon has actually stopped answering — the **stop
/// barrier** (REQ-HAZARD-DAEMON-STOP-BARRIER / B3). The `stopping` ack is sent
/// BEFORE the serve loop drops its listener, so returning on the bare ack leaves
/// a window where the socket is still bound: a following `is_running` ping (the
/// `daemon start` liveness check) wins that window and start falsely reports
/// ALREADY_RUNNING. We close it by polling [`ping`] to failure after the ack —
/// when the probe can no longer reach a listener, the stop has fully completed
/// and an immediate restart is safe.
// [impl->REQ-CLI-2]
// [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]
pub fn request_stop(name: &str) -> io::Result<()> {
    let mut conn = connect(name)?;
    write_frame(
        &mut conn,
        &Envelope::new(KIND_STOP, serde_json::Value::Null),
    )?;
    let env = read_frame(&mut conn)?;
    if env.kind != KIND_STOPPING {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "expected stopping ack",
        ));
    }
    drop(conn); // release our connection so it never props the loop open
    // Barrier: the ack is not the stop — wait until the listener is truly gone.
    let mut waited = std::time::Duration::ZERO;
    while waited < STOP_BARRIER_TIMEOUT {
        if ping(name).is_err() {
            return Ok(()); // no listener answers — stop has fully completed
        }
        std::thread::sleep(STOP_BARRIER_POLL);
        waited += STOP_BARRIER_POLL;
    }
    Err(io::Error::new(
        io::ErrorKind::TimedOut,
        "daemon acked stopping but kept answering past the stop barrier",
    ))
}

+/// How long [`request_stop`] waits for the seed socket to actually go away after

+/// the `stopping` ack before giving up. The serve loop returns within one accept

+/// of the stop latch (the throwaway dial wakes it), so this only ever spends a

+/// few poll slices — the ceiling is a wedge guard, not the expected cost.

+const STOP_BARRIER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);

+const STOP_BARRIER_POLL: std::time::Duration = std::time::Duration::from_millis(10);

+/// Returns only once the daemon has actually stopped answering — the **stop

+/// barrier** (REQ-HAZARD-DAEMON-STOP-BARRIER / B3). The `stopping` ack is sent

+/// BEFORE the serve loop drops its listener, so returning on the bare ack leaves

+/// a window where the socket is still bound: a following `is_running` ping (the

+/// `daemon start` liveness check) wins that window and start falsely reports

+/// ALREADY_RUNNING. We close it by polling [`ping`] to failure after the ack —

+/// when the probe can no longer reach a listener, the stop has fully completed

+    drop(conn); // release our connection so it never props the loop open

+    // Barrier: the ack is not the stop — wait until the listener is truly gone.

+    let mut waited = std::time::Duration::ZERO;

+    while waited < STOP_BARRIER_TIMEOUT {

+            return Ok(()); // no listener answers — stop has fully completed

+        std::thread::sleep(STOP_BARRIER_POLL);

+        "daemon acked stopping but kept answering past the stop barrier",

/// Probe whether a daemon is serving the seed-control channel `name` (the


Now the B3 unit — assert the post-return ping is `Err` with no poll loop (the barrier guarantee):


    // [unit->REQ-START-3] a seed PUT over IPC is TAKEn once: the second take
    // misses (consume-once), proving the in-memory map replaces the file bridge.
    #[test]
    fn put_then_take_consumes_once() {

    // [unit->REQ-HAZARD-DAEMON-STOP-BARRIER] the stop barrier: request_stop does
    // NOT return on the bare `stopping` ack — it blocks until the listener is
    // actually gone, so an IMMEDIATE post-return ping finds no daemon (no poll
    // loop). Without the barrier this ping would race the still-bound socket and
    // a `daemon start` would falsely see ALREADY_RUNNING.
    #[test]
    fn request_stop_barrier_holds_until_no_listener() {
        with_home(|_| {
            let reg = Arc::new(SeedRegistry::new());
            start(Arc::clone(&reg));
            let name = seed_socket_name();

            request_stop(&name).expect("stop acked + barrier passed");
            // The barrier guarantees this without any retry loop.
            assert!(
                ping(&name).is_err(),
                "no listener may answer the instant request_stop returns"
            );
        });
    }

    // [unit->REQ-START-3] a seed PUT over IPC is TAKEn once: the second take
    // misses (consume-once), proving the in-memory map replaces the file bridge.
    #[test]
    fn put_then_take_consumes_once() {


    // [unit->REQ-HAZARD-DAEMON-STOP-BARRIER] the stop barrier: request_stop does
    // NOT return on the bare `stopping` ack — it blocks until the listener is
    // actually gone, so an IMMEDIATE post-return ping finds no daemon (no poll
    // loop). Without the barrier this ping would race the still-bound socket and
    // a `daemon start` would falsely see ALREADY_RUNNING.
    #[test]
    fn request_stop_barrier_holds_until_no_listener() {
        with_home(|_| {
            let reg = Arc::new(SeedRegistry::new());
            start(Arc::clone(&reg));
            let name = seed_socket_name();

            request_stop(&name).expect("stop acked + barrier passed");
            // The barrier guarantees this without any retry loop.
            assert!(
                ping(&name).is_err(),
                "no listener may answer the instant request_stop returns"
            );
        });
    }

    // [unit->REQ-START-3] a seed PUT over IPC is TAKEn once: the second take
    // misses (consume-once), proving the in-memory map replaces the file bridge.
    #[test]
    fn put_then_take_consumes_once() {

+    // [unit->REQ-HAZARD-DAEMON-STOP-BARRIER] the stop barrier: request_stop does

+    // NOT return on the bare `stopping` ack — it blocks until the listener is

+    // actually gone, so an IMMEDIATE post-return ping finds no daemon (no poll

+    // loop). Without the barrier this ping would race the still-bound socket and

+    // a `daemon start` would falsely see ALREADY_RUNNING.

+    fn request_stop_barrier_holds_until_no_listener() {

+            let reg = Arc::new(SeedRegistry::new());

+            let name = seed_socket_name();

+            request_stop(&name).expect("stop acked + barrier passed");

+            // The barrier guarantees this without any retry loop.

+                "no listener may answer the instant request_stop returns"


Now wire the reaper. First, brain spawns its own process group on Unix (`spawn_brain_child`):


    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
        cmd.creation_flags(CREATE_NO_WINDOW);
    }
    cmd.spawn()
}

    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
        cmd.creation_flags(CREATE_NO_WINDOW);
    }
    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its
    // OWN process group so the daemon can reap the brain + everything it spawns
    // (the shellwake watchers, the detached Psyches — all inherit this group) as
    // one unit on stop, without signalling the daemon itself. Windows roots the
    // reap on a Job object instead (assigned in the supervisor via BrainReaper).
    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        unsafe {
            cmd.pre_exec(|| {
                // setpgid(0, 0): this child becomes the leader of a new group
                // (pgid == its pid). Best-effort — a failure only loses the reap.
                libc::setpgid(0, 0);
                Ok(())
            });
        }
    }
    cmd.spawn()
}


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

    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
        cmd.creation_flags(CREATE_NO_WINDOW);
    }
    cmd.spawn()
}

    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
        cmd.creation_flags(CREATE_NO_WINDOW);
    }
    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its
    // OWN process group so the daemon can reap the brain + everything it spawns
    // (the shellwake watchers, the detached Psyches — all inherit this group) as
    // one unit on stop, without signalling the daemon itself. Windows roots the
    // reap on a Job object instead (assigned in the supervisor via BrainReaper).
    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        unsafe {
            cmd.pre_exec(|| {
                // setpgid(0, 0): this child becomes the leader of a new group
                // (pgid == its pid). Best-effort — a failure only loses the reap.
                libc::setpgid(0, 0);
                Ok(())
            });
        }
    }
    cmd.spawn()
}

+    // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the brain leads its

+    // OWN process group so the daemon can reap the brain + everything it spawns

+    // (the shellwake watchers, the detached Psyches — all inherit this group) as

+    // one unit on stop, without signalling the daemon itself. Windows roots the

+    // reap on a Job object instead (assigned in the supervisor via BrainReaper).

+        use std::os::unix::process::CommandExt;

+                // setpgid(0, 0): this child becomes the leader of a new group

+                // (pgid == its pid). Best-effort — a failure only loses the reap.


Now `spawn_brain_supervisor` — accept the reaper and enroll each spawn:


pub fn spawn_brain_supervisor(stop: Arc<AtomicBool>) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {
    let restart = Arc::new(BrainRestart::new());
    let restart_sup = Arc::clone(&restart);

pub fn spawn_brain_supervisor(
    stop: Arc<AtomicBool>,
    reaper: Arc<crate::reap::BrainReaper>,
) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {
    let restart = Arc::new(BrainRestart::new());
    let restart_sup = Arc::clone(&restart);


pub fn spawn_brain_supervisor(stop: Arc<AtomicBool>) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {
    let restart = Arc::new(BrainRestart::new());
    let restart_sup = Arc::clone(&restart);

pub fn spawn_brain_supervisor(
    stop: Arc<AtomicBool>,
    reaper: Arc<crate::reap::BrainReaper>,
) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {
    let restart = Arc::new(BrainRestart::new());
    let restart_sup = Arc::clone(&restart);

/// Returns the shared [`BrainRestart`] signal so the caller can hand a clone to

/// the broker, wiring the `KIND_BRAIN_RESTART` verb to this supervisor (D3-3).

-pub fn spawn_brain_supervisor(stop: Arc<AtomicBool>) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {

+    reaper: Arc<crate::reap::BrainReaper>,

+) -> (thread::JoinHandle<()>, Arc<BrainRestart>) {

// KH 6.11: capture the canonical brain binary path ONCE here, at broker


            SUPERVISE_HEALTHY_RUN,
            |generation, reason, binary| {
                spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())
            },
        );
    });
    (handle, restart)

            SUPERVISE_HEALTHY_RUN,
            |generation, reason, binary| {
                let child =
                    spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;
                // Enroll the (re)spawned brain into the reap container so a
                // `daemon stop` tears down the brain + its Psyches as one unit
                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]
                reaper.enroll(&child);
                Ok(child)
            },
        );
    });
    (handle, restart)


            SUPERVISE_HEALTHY_RUN,
            |generation, reason, binary| {
                spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())
            },
        );
    });
    (handle, restart)

            SUPERVISE_HEALTHY_RUN,
            |generation, reason, binary| {
                let child =
                    spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;
                // Enroll the (re)spawned brain into the reap container so a
                // `daemon stop` tears down the brain + its Psyches as one unit
                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]
                reaper.enroll(&child);
                Ok(child)
            },
        );
    });
    (handle, restart)

-                spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())

+                    spawn_brain_child(generation, reason, binary, canonical_exe.as_deref())?;

+                // Enroll the (re)spawned brain into the reap container so a

+                // `daemon stop` tears down the brain + its Psyches as one unit

+                // (REQ-HAZARD-DAEMON-STOP-REAP). [impl->REQ-HAZARD-DAEMON-STOP-REAP]


Now daemon.rs — create the reaper, hoist the supervisor stop flag, and reap after the serve loop returns:


        {
            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(stop);
            if let Some(broker) = &broker_handle {
                broker.set_brain_restart(restart);
            }
        }

        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap
        // container the supervisor enrolls every (re)spawned brain into. Created
        // here so it outlives the supervisor and is reachable from the
        // graceful-stop path below. The brain subtree (the brain + its shellwake
        // watchers + its detached Psyches) reaps as one unit; the daemon itself is
        // never in the container.
        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]
        let reaper = crate::reap::BrainReaper::new();
        // The supervisor stop flag — hoisted out of the wiring block (was held
        // "for symmetry, never raised") because the graceful-stop path NOW raises
        // it, so the supervisor does not respawn the brain we are about to reap.
        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
        {
            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(
                Arc::clone(&brain_stop),
                Arc::clone(&reaper),
            );
            if let Some(broker) = &broker_handle {
                broker.set_brain_restart(restart);
            }
        }


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

        {
            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(stop);
            if let Some(broker) = &broker_handle {
                broker.set_brain_restart(restart);
            }
        }

        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap
        // container the supervisor enrolls every (re)spawned brain into. Created
        // here so it outlives the supervisor and is reachable from the
        // graceful-stop path below. The brain subtree (the brain + its shellwake
        // watchers + its detached Psyches) reaps as one unit; the daemon itself is
        // never in the container.
        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]
        let reaper = crate::reap::BrainReaper::new();
        // The supervisor stop flag — hoisted out of the wiring block (was held
        // "for symmetry, never raised") because the graceful-stop path NOW raises
        // it, so the supervisor does not respawn the brain we are about to reap.
        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
        {
            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(
                Arc::clone(&brain_stop),
                Arc::clone(&reaper),
            );
            if let Some(broker) = &broker_handle {
                broker.set_brain_restart(restart);
            }
        }

// for symmetry, never raised (the daemon runs until killed). The brain

// child inherits this process's already-unelevated token.

+        // Daemon-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the reap

+        // container the supervisor enrolls every (re)spawned brain into. Created

+        // here so it outlives the supervisor and is reachable from the

+        // graceful-stop path below. The brain subtree (the brain + its shellwake

+        // watchers + its detached Psyches) reaps as one unit; the daemon itself is

+        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]

+        let reaper = crate::reap::BrainReaper::new();

+        // The supervisor stop flag — hoisted out of the wiring block (was held

+        // "for symmetry, never raised") because the graceful-stop path NOW raises

+        // it, so the supervisor does not respawn the brain we are about to reap.

+        let brain_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));

-            let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));

-            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(stop);

+            let (_handle, restart) = crate::brainproc::spawn_brain_supervisor(

+                Arc::clone(&brain_stop),


        // The seed-control channel is the foreground loop. Binding it is what
        // arbitrates the single-daemon-per-home invariant: a second daemon racing
        // to start fails this bind and exits, leaving the winner serving.
        seedmap::serve_seed_control(&seed_socket_name(), reg)
    }

        // The seed-control channel is the foreground loop. Binding it is what
        // arbitrates the single-daemon-per-home invariant: a second daemon racing
        // to start fails this bind and exits, leaving the winner serving. It
        // returns only on a graceful `daemon stop` (the KIND_STOP latch).
        let serve = seedmap::serve_seed_control(&seed_socket_name(), reg);

        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop
        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it
        // does not respawn the brain we are about to kill) THEN reap the brain
        // subtree — the brain, its shellwake watchers, and its detached Psyches die
        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /
        // spt.exe. The daemon is not in the reap container, so this never
        // self-terminates: it returns cleanly below.
        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]
        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);
        reaper.reap();
        serve
    }


        // The seed-control channel is the foreground loop. Binding it is what
        // arbitrates the single-daemon-per-home invariant: a second daemon racing
        // to start fails this bind and exits, leaving the winner serving.
        seedmap::serve_seed_control(&seed_socket_name(), reg)
    }

        // The seed-control channel is the foreground loop. Binding it is what
        // arbitrates the single-daemon-per-home invariant: a second daemon racing
        // to start fails this bind and exits, leaving the winner serving. It
        // returns only on a graceful `daemon stop` (the KIND_STOP latch).
        let serve = seedmap::serve_seed_control(&seed_socket_name(), reg);

        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop
        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it
        // does not respawn the brain we are about to kill) THEN reap the brain
        // subtree — the brain, its shellwake watchers, and its detached Psyches die
        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /
        // spt.exe. The daemon is not in the reap container, so this never
        // self-terminates: it returns cleanly below.
        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]
        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);
        reaper.reap();
        serve
    }

-        // to start fails this bind and exits, leaving the winner serving.

-        seedmap::serve_seed_control(&seed_socket_name(), reg)

+        // to start fails this bind and exits, leaving the winner serving. It

+        // returns only on a graceful `daemon stop` (the KIND_STOP latch).

+        let serve = seedmap::serve_seed_control(&seed_socket_name(), reg);

+        // Graceful-stop reap (REQ-HAZARD-DAEMON-STOP-REAP / Breap): the serve loop

+        // returned because a `daemon stop` acked. Stop the supervisor FIRST (so it

+        // does not respawn the brain we are about to kill) THEN reap the brain

+        // subtree — the brain, its shellwake watchers, and its detached Psyches die

+        // with the daemon instead of orphaning as ~8 stray claude-spt-psyche.exe /

+        // spt.exe. The daemon is not in the reap container, so this never

+        // self-terminates: it returns cleanly below.

+        // [impl->REQ-HAZARD-DAEMON-STOP-REAP]

+        brain_stop.store(true, std::sync::atomic::Ordering::SeqCst);

/// Run the brain child (`spt daemon brain`, hidden — ADR-0018 D1). The


Now compile-check spt-daemon in the warm scratch target (serial, single invocation per the HFENDULEAM lesson):


Bash

CARGO_TARGET_DIR=target-seam cargo check -p spt-daemon --all-targets 2>&1 | tail -40

Check-build spt-daemon in scratch target


Command running in background with ID: bkjz6xr2f. 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\bkjz6xr2f.output. You will be notified when it completes. To check interim output, use Read on that file path.


ToolSearch

Question: select:TaskCreate


Question: select:TaskCreate


Skip task-tracking overhead — impl is nearly done; focus on the seam bar. Check the build:


Bash

Read check-build output


(Bash completed with no output)


<task-notification>
<task-id>bkjz6xr2f</task-id>
<tool-use-id>toolu_01J3SboeNMnV5GvXMZCS8gsF</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\bkjz6xr2f.output</output-file>
<status>completed</status>
<summary>Background command "Check-build spt-daemon in scratch target" completed (exit code 0)</summary>
</task-notification>


sleep 20; echo "=== output ==="; cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bkjz6xr2f.output" 2>&1 | tail -30; echo "=== done marker ==="

Wait and read build output


=== output ===
    Checking ipconfig v0.3.4
    Checking tokio v1.52.3
    Checking tokio-util v0.7.18
    Checking tokio-rustls v0.26.4
    Checking tower v0.5.3
    Checking acto v0.8.2
    Checking backon v1.6.0
    Checking tower-http v0.6.11
    Checking swarm-discovery v0.6.0-alpha.2
    Checking h2 v0.4.14
    Checking n0-future v0.3.2
    Checking tokio-stream v0.1.18
    Checking tokio-websockets v0.13.2
    Checking n0-watcher v0.6.1
    Checking iroh-dns v0.98.0
    Checking netwatch v0.16.0
    Checking noq v0.18.0
    Checking hyper v1.10.1
    Checking hickory-net v0.26.0-beta.4
    Checking hyper-util v0.1.20
    Checking hickory-resolver v0.26.0-beta.4
    Checking hyper-rustls v0.27.9
    Checking igd-next v0.17.1
    Checking reqwest v0.13.4
    Checking portmapper v0.16.0
    Checking iroh-relay v0.98.0
    Checking iroh v0.98.2
    Checking spt-net v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-daemon v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.47s
=== done marker ===


Prompt:
<task-notification>
<task-id>bkjz6xr2f</task-id>
<tool-use-id>toolu_01J3SboeNMnV5GvXMZCS8gsF</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\bkjz6xr2f.output</output-file>
<status>completed</status>
<summary>Background command "Check-build spt-daemon in scratch target" completed (exit code 0)</summary>
</task-notification>


1	    Checking noq-proto v0.17.0
2	    Checking socket2 v0.6.4
3	    Checking mio v1.2.1
4	    Checking rustls-platform-verifier v0.7.0
5	    Checking netdev v0.42.0
6	    Checking seize v0.5.1
7	    Checking interprocess v2.4.2
8	    Checking tempfile v3.27.0
9	    Checking papaya v0.2.4
10	    Checking noq-udp v0.10.0
11	    Checking ipconfig v0.3.4
12	    Checking tokio v1.52.3
13	    Checking tokio-util v0.7.18
14	    Checking tokio-rustls v0.26.4
15	    Checking tower v0.5.3
16	    Checking acto v0.8.2
17	    Checking backon v1.6.0
18	    Checking tower-http v0.6.11
19	    Checking swarm-discovery v0.6.0-alpha.2
20	    Checking h2 v0.4.14
21	    Checking n0-future v0.3.2
22	    Checking tokio-stream v0.1.18
23	    Checking tokio-websockets v0.13.2
24	    Checking n0-watcher v0.6.1
25	    Checking iroh-dns v0.98.0
26	    Checking netwatch v0.16.0
27	    Checking noq v0.18.0
28	    Checking hyper v1.10.1
29	    Checking hickory-net v0.26.0-beta.4
30	    Checking hyper-util v0.1.20
31	    Checking hickory-resolver v0.26.0-beta.4
32	    Checking hyper-rustls v0.27.9
33	    Checking igd-next v0.17.1
34	    Checking reqwest v0.13.4
35	    Checking portmapper v0.16.0
36	    Checking iroh-relay v0.98.0
37	    Checking iroh v0.98.2
38	    Checking spt-net v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
39	    Checking spt-daemon v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
40	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.47s


Clean compile. Now the seam bar — start with the exact CI clippy invocation (workspace-wide, per the pre-flight lesson):


CARGO_TARGET_DIR=target-seam cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -40

Workspace clippy, deny warnings (CI invocation)


Command running in background with ID: brgw7ceyr. 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\brgw7ceyr.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>brgw7ceyr</task-id>
<tool-use-id>toolu_01R6fpkXXX4ePBiYKjayRuFS</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\brgw7ceyr.output</output-file>
<status>completed</status>
<summary>Background command "Workspace clippy, deny warnings (CI invocation)" completed (exit code 0)</summary>
</task-notification>


Poll clippy output


<tool_use_error>Blocked: sleep 60 followed by: tail -30 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\brgw7ceyr.output". To wait for a condition, use Monitor with an until-loop (e.g. `until <check>; do sleep 2; done`). To wait for a command you started, use run_in_background: true. Do not chain shorter sleeps to work around this block.</tool_use_error>

Error: Blocked: sleep 60 followed by: tail -30 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\brgw7ceyr.output". To wait for a condition, use Monitor with an until-loop (e.g. `until <check>; do sleep 2; done`). To wait for a command you started, use run_in_background: true. Do not chain shorter sleeps to work around this block.


Foreground sleep blocked — I'll await the clippy completion notification. Meanwhile, confirm how `traceable-reqs` is invoked here (file scan, won't contend on the build lock):


command -v traceable-reqs 2>/dev/null && echo "ON-PATH" || echo "not on path"; ls -la .traceable* traceable-reqs.toml 2>/dev/null | head; grep -rn "traceable-reqs" .github/workflows/*.yml 2>/dev/null | head

Find traceable-reqs invocation


/c/Users/decid/AppData/Local/Programs/traceable-reqs/traceable-reqs
ON-PATH
-rw-r--r-- 1 decid 197609 207981 Jun 18 01:06 traceable-reqs.toml
.github/workflows/ci.yml:314:      - name: Install traceable-reqs
.github/workflows/ci.yml:323:          BIN="$HOME/.local/bin/traceable-reqs"
.github/workflows/ci.yml:325:            ln -sf "$BIN" ./traceable-reqs
.github/workflows/ci.yml:332:              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" "$SRC"
.github/workflows/ci.yml:335:            cp "$SRC/rust/target/release/traceable-reqs" "$BIN"
.github/workflows/ci.yml:336:            ln -sf "$BIN" ./traceable-reqs
.github/workflows/ci.yml:338:          ./traceable-reqs --version
.github/workflows/ci.yml:341:          # read BigscreenVR/traceable-reqs. Supply a PAT with read access to it
.github/workflows/ci.yml:345:        run: ./traceable-reqs check --json
.github/workflows/ci.yml:347:        run: ./traceable-reqs lint || true


Prompt:
<task-notification>
<task-id>brgw7ceyr</task-id>
<tool-use-id>toolu_01R6fpkXXX4ePBiYKjayRuFS</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\brgw7ceyr.output</output-file>
<status>completed</status>
<summary>Background command "Workspace clippy, deny warnings (CI invocation)" completed (exit code 0)</summary>
</task-notification>


Clippy clean (exit 0). `traceable-reqs` is a standalone binary (file scan, no build contention). Run it + the targeted units:


echo "=== traceable check ==="; traceable-reqs check; echo "EXIT=$?"

Run traceable-reqs check


<persisted-output>
Output too large (100.1KB). Full output saved to: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\2d6a1938-1527-4cab-bf64-9f0d623c3955\tool-results\bwi2r1kh5.txt

Preview (first 2KB):
=== traceable check ===
244 requirements: 244 complete, 0 incomplete, 0 findings
  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       api prefix and adapter_name on every machinery invocation
  [OK] REQ-API-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       The api subcommand surface (bind/listen/poll/state/worker/boundary/...)
  [OK] REQ-API-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       commune/signoff are file-drops, not commands
  [OK] REQ-API-4  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       api resolves the adapter manifest (+ profile + install dir) from `--adapter name:profile` via the registry when `--manifest` is omitted; `--manifest` becomes an optional OVERRIDE (unregistered / local-dev manifests). Removes the require-both-flags redundancy — a registered adapter's live bringup / digest / capability needs only `--adapter` — and yields the precise install dir (the record's source_dir) rather than the --manifest parent, closing the copy-mode psyche-binary edge (v0.8.0)
  [OK] REQ-ARCH-1  required: [impl]  stages: -doc +impl -unit -int
       Many small acyclically-layered crates
  [OK] REQ-ARCH-2  required: [impl]  stages: -doc +impl -unit -int
       Public SDK surface is spt-proto, spt-runtime, spt-msg
  [OK] REQ-ARCH-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Wire-protocol version independent of crate semver, N-1 compat window
  [OK] REQ-ARCH-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Copy-verbatim the commodity layer from the sister project
  [OK] REQ-CLI-1  required: [impl, unit]  stages: -doc +impl +unit -int
...
</persisted-output>

=== traceable check ===
244 requirements: 244 complete, 0 incomplete, 0 findings
  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       api prefix and adapter_name on every machinery invocation
  [OK] REQ-API-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       The api subcommand surface (bind/listen/poll/state/worker/boundary/...)
  [OK] REQ-API-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       commune/signoff are file-drops, not commands
  [OK] REQ-API-4  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       api resolves the adapter manifest (+ profile + install dir) from `--adapter name:profile` via the registry when `--manifest` is omitted; `--manifest` becomes an optional OVERRIDE (unregistered / local-dev manifests). Removes the require-both-flags redundancy — a registered adapter's live bringup / digest / capability needs only `--adapter` — and yields the precise install dir (the record's source_dir) rather than the --manifest parent, closing the copy-mode psyche-binary edge (v0.8.0)
  [OK] REQ-ARCH-1  required: [impl]  stages: -doc +impl -unit -int
       Many small acyclically-layered crates
  [OK] REQ-ARCH-2  required: [impl]  stages: -doc +impl -unit -int
       Public SDK surface is spt-proto, spt-runtime, spt-msg
  [OK] REQ-ARCH-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Wire-protocol version independent of crate semver, N-1 compat window
  [OK] REQ-ARCH-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Copy-verbatim the commodity layer from the sister project
  [OK] REQ-CLI-1  required: [impl, unit]  stages: -doc +impl +unit -int
       spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet <name>] grouped by subnet with SELF pinned, --detail adding the ex-resources yellow-pages blurb projection; bare spt endpoint = the list (M8 decisions 1-2, 25)
  [OK] REQ-CLI-2  required: [impl, unit]  stages: -doc +impl +unit -int
       spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23)
  [OK] REQ-CLI-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Agent hot path stays flat across the M8 reorg: send/ring/ready/whoami/how-to unchanged; notify moves to subnet notify while notif stays top-level; breaking renames land clean with no deprecation shims (zero external CLI consumers pre-spt-claude-code) (M8 decisions 3-4, 9)
  [OK] REQ-CLI-4  required: []  stages: -doc +impl -unit -int
       User-facing CLI output is human-readable: DIRECT-USER commands (e.g. adapter update/list/use) render friendly prose instead of raw CODE:RESULT markers — "claude-spt is up to date (0.2.0)." not "ADAPTER_UPDATE_UPTODATE:claude-spt: installed 0.2.0, latest 0.2.0". Strictly bounded to the direct-user surface: the adapter-PARSED bringup tokens (SEEDED/BOUND/READY/NO_SEED on seed/listen, which adapters grep) stay machine-parseable — humanization is additive (a human line beside the marker, or a --porcelain/--quiet split), never a silent rename of a dual-contract marker. The user-facing bringup composition belongs to the adapter (perri); this REQ owns only the direct-user CLI surface. (v0.9.0)
  [OK] REQ-CONSENT-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Consent grant store: capability x subject-agent x target-node rows, enforced at the target node, subnet-settable (replicates as security material near the trust store), revocable; gated-capability ids (remote-exec, instantiate-anywhere) reserved-but-refusing; v1 consumers are the shell spawn gates (CONTEXT Consent & security gates)
  [OK] REQ-CONSENT-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Interactive consent escalation: an ungated high-risk action routes a consent prompt to the user's most-recently-active session; allow-once / allow-always (writes a grant) / deny; pre-consent flags (can_shutdown, shell_wake_spawn_anywhere) author grants via manifest/settings (CONTEXT Consent & security gates)
  [OK] REQ-CONSENT-3  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Per-capability approval gates (class-keyed): the require_approval enum may ride INDIVIDUAL [shell.capabilities] entries — gating the dangerous ACT, not just the spawn — with an optional class_key scoping the grant qualifier finer than the capability id ((owner endpoint x device class x node); a remembered HID-class attach grant never authorizes a storage-class attach). Reuses the grant store + interactive escalation + tighten-only floor (REQ-CONSENT-1/2 plumbing). Spawn gates govern EXISTENCE; capability gates govern ACTS — an explicitly distinct invariant (CONTEXT:283, ratified 2026-06-11 Gateway grill).
  [OK] REQ-CONV-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
  [OK] REQ-CONV-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Event-driven advertisement: endpoint online/offline transitions (ready-listener start/stop, rest-state transition, perch death) trigger an immediate advertise_local + peer push as a WAKE of the existing pump loop (no second advertisement path — epoch lease + visibility gates ride unchanged); the cadence stays the steady-state floor (M8 decision 15)
  [OK] REQ-DAEMON-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       One per-machine spt-daemon owning all per-machine state
  [OK] REQ-DAEMON-2  required: [impl, unit, int]  stages: +doc +impl +unit +int
       Broker/brain split for seamless self-update
  [OK] REQ-DAEMON-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Any api invocation auto-starts the daemon if absent
  [OK] REQ-DAEMON-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Honor every KNOWN-HAZARDS invariant
  [OK] REQ-DAEMON-5  required: [impl, unit]  stages: -doc +impl +unit -int
       Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death)
  [OK] REQ-DAEMON-6  required: [impl, unit]  stages: -doc +impl +unit -int
       Service-aware `daemon start`/`stop`: when an OS service manager has a registered spt-daemon for this user, `spt daemon start` and `spt daemon stop` drive THAT service (so stop doesn't IPC-kill a unit that auto-restart-fights for the broker socket — the kitsubito 2026-06-08 loop). `start` graduates from a `run` alias to a first-class background verb (ensure-up, idempotent, non-blocking); stop routes managed→manager, manual→IPC. Linux=systemd user unit (`systemctl --user start|stop|is-active spt-daemon`, detected by unit-file presence); Windows=no controllable manager (the logon task is boot-only), so start=detached spawn / stop=IPC.
  [OK] REQ-DAEMON-7  required: [impl, unit]  stages: -doc +impl +unit -int
       `daemon run` is foreground-consistent on every platform: the invoking process IS the daemon, blocks until signalled, never auto-detaches or respawns into an invisible background task. The detached/de-elevated background behavior lives ONLY in `start`. Windows: an ELEVATED `daemon run` refuses with guidance (use `start`, or an unelevated shell) instead of respawning detached/de-elevated and vanishing (KH 5.7 preserved — it still never serves elevated).
  [OK] REQ-DAEMON-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Internal auto-start prefers the service: `ensure_running` (any spt command's implicit daemon start, REQ-DAEMON-3) routes through the service-aware start path — when a manager has a registered service it starts THAT, never a competing manual `spawn_detached` daemon that would fight the service for the socket.
  [OK] REQ-DAEMON-9  required: [impl, unit]  stages: -doc +impl +unit -int
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
  [OK] REQ-DOCS-1  required: [doc, impl]  stages: +doc +impl -unit -int
       Dual-audience docs (human + AI dev-agent), markdown once / two depths
  [OK] REQ-DOCS-2  required: [doc, int]  stages: +doc -impl -unit +int
       Sub-10-minute runnable killer quickstart per audience
  [OK] REQ-DOCS-3  required: [doc]  stages: +doc -impl -unit -int
       Diátaxis structure; one canonical way to do X
  [OK] REQ-DOCS-4  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)
  [OK] REQ-DOCS-5  required: [impl, int]  stages: -doc +impl -unit +int
       Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked
  [OK] REQ-DOCS-6  required: [impl, unit, int]  stages: +doc +impl +unit +int
       spt how-to <topic>: in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it)
  [OK] REQ-ELEVATE-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Cross-platform self-elevating re-launch for privilege-gated commands: a pure decision seam `decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath{AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint}` selecting how to re-acquire privilege, and the per-OS impure launchers it dispatches — Windows UAC console (ShellExecuteW `runas` on the abs-exe + verbatim argv; the elevated child does the work, prints 'You can close this window', and pauses for a keypress; the original prints 'Elevated terminal launched…' and exits 0; NEVER pipes the child's stdout back across the privilege boundary), Linux desktop pkexec (preferred, native polkit GUI auth) else x-terminal-emulator -e sudo (fallback list x-terminal-emulator→gnome-terminal→konsole→xterm), the existing interactive-TTY inline sudo, and the headless/no-path floor that prints the absolute-path command. Reused by every gated command (not subnet-specific). Generalizes should_auto_elevate.
  [OK] REQ-ENDPOINT-STOP-OFFLINE  required: [impl, unit]  stages: -doc +impl +unit -int
       H3: `spt endpoint stop <id>` marks the endpoint OFFLINE (alive=false), not merely de-readied. cmd_stop (cli.rs:2994-3010) removes the ready marker + unregisters the address but does NOT set status offline, so a stopped daemon-hosted endpoint still reports alive=true (status=online latch). FIX: add set_status(perch, STATUS_OFFLINE) to cmd_stop — folds with B2 (same setter). Unit: stop → is_perch_alive=false / alive=false. (v0.12.0)
  [OK] REQ-EP-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Day-one endpoint types; open type system
  [OK] REQ-EP-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Agent endpoints vs Shells distinction in the type model
  [OK] REQ-EP-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Messaging payloads carry typed operation commands + file blobs
  [OK] REQ-EP-4  required: [impl, unit]  stages: -doc +impl +unit -int
       PresenceChannel broker endpoint (seam day-one)
  [OK] REQ-EP-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope
  [OK] REQ-EP-6  required: [doc, impl, unit]  stages: +doc +impl +unit +int
       Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6
  [OK] REQ-EP-7  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Durable live-role.md: a per-agent broad-purpose statement in tracked/agents/<id>/ beside live-context.md (replicates with the mind on the same a-<id> branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite <file>` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing)
  [OK] REQ-FRONT-1  required: []  stages: -doc -impl -unit -int
       Day-one launcher/manager frontend (list/launch/attach/init)
  [OK] REQ-HAZARD-BRAIN-RESPAWN-PATH  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silently assumed path-string semantics). Backstop: promotion gates on bytes — a trial promotes only if brain.ready exe_hash == the staged artifact hash for this platform, else auto-rollback + loud notif (readiness != new-bytes was the false-success that recorded applied:8 over a v0.4.0 brain on kitsubito, 2026-06-11). KNOWN-HAZARDS 6.11.
  [OK] REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE  required: []  stages: -doc -impl -unit -int
       B4 (deepest): a bare brain restart (broker survives) REHYDRATES the live-agent lifecycle so post-restart endpoints are hosted + attachable. Today resume_sessions (brainproc.rs:186, brain.rs:797-809) re-subscribes to the broker's PTY sessions but ALL BrainLifecycle instances (lifecycle.rs:58-130; the ephemeral brain.rs:254-275) are LOST on restart → a post-restart live endpoint gets no livehost → its Psyche is never (re)hosted and new spawns die / can't attach until a FULL daemon reset (operator: perri's brain kill+restart wedged everything until a full daemon kill). FIX: on brain startup, rebuild a BrainLifecycle per resumed live-capable session — load the manifest from the adapter registry → instantiate → start the pulse — the rehydrate the resume no-op cannot do. Composes with B2 (the reconcile re-hosts from the honest on-disk status after rehydrate). (v0.12.0)
  [OK] REQ-HAZARD-BROKER-PROCESS-ISOLATION  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Broker and brain are separate processes: the broker runs as its own long-lived per-machine process that survives every brain restart, so a routine (brain-only) self-update restarts the brain onto the swapped binary while every hosted endpoint (PTY child, live QUIC conn, listening socket) stays untouched at the PROCESS level. The in-process-thread broker (daemon.rs:165-170) is a regression that silently unrealizes REQ-UPD-3 — apply degrades to an in-process Brain::handoff no-op and new code does not run until an unrelated restart (KNOWN-HAZARDS 6.7). Evidence must prove process-level survival (SPIKE-01/03 productionized as int: PTY child + live QUIC survive a brain-PROCESS restart onto a swapped binary), re-pointing the regression-masked in-process int tags currently on REQ-DAEMON-2 / REQ-UPD-3 (ADR-0018).
  [OK] REQ-HAZARD-BROKER-QUIC-DEADLINE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16.
  [OK] REQ-HAZARD-BROKER-SEED-WIRE-SKEW  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       A daemon-state wire-format change (e.g. the v0.9.0 adapter-agnostic Seed) does NOT take effect until a DELIBERATE full broker restart: the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004 no-terminate-during-update forbids auto-killing it), so a NEW-version CLI talking to a still-resident OLD broker fails the seed handshake — the old broker cannot deserialize the new Seed (its formerly-required `adapter` field is gone) and drops the conn without an ack, which surfaces to the CLI as a raw UnexpectedEof 'failed to fill whole buffer'. spt-core must (a) surface an ACTIONABLE diagnostic on that seed-ack EOF (name the stale-broker cause + the `spt daemon stop` fix — the broker restarts on the next api call), never the cryptic io error; and (b) document the operational rule (a deliberate broker restart is required on any daemon-state wire change — NOT automatic) + the FORWARD discipline (daemon-state/Seed schema changes stay additive + serde-default so a resident OLD broker tolerates a NEW CLI across a brain-only update; note this would NOT have rescued 0.9.0 itself, since the old broker's `adapter` was a required field). perri PREP-4 FINDING 1 (v0.9.0 CLI vs stale 0.8.x broker).
  [OK] REQ-HAZARD-CASCADE-WIPE-GUARD  required: [impl, unit]  stages: -doc +impl +unit -int
       No hard-delete of a parent hosting non-empty children (6.3)
  [OK] REQ-HAZARD-CHILD-CONSOLE-FLASH  required: [impl, unit]  stages: +doc +impl +unit -int
       Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)
  [OK] REQ-HAZARD-CONFLICT-BOTH-PRESERVED  required: [impl, unit]  stages: -doc +impl +unit -int
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
  [OK] REQ-HAZARD-CONPTY-DSR  required: [impl, unit]  stages: -doc +impl +unit +int
       ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)
  [OK] REQ-HAZARD-DAEMON-HOSTED-LIVENESS  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)
  [OK] REQ-HAZARD-DAEMON-SCHED-NONBLOCKING  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)
  [OK] REQ-HAZARD-DAEMON-STOP-BARRIER  required: [impl, unit]  stages: -doc +impl +unit -int
       B3: `spt daemon stop` then an immediate `spt daemon start` does NOT race — stop fully completes before it returns. Today request_stop (seedmap.rs:240-255) returns on the KIND_STOPPING ack (sent seedmap.rs:174-176) BEFORE the seed socket unbinds, so a following is_running ping (daemon.rs:375) wins the exit window and start reports ALREADY_RUNNING (operator: daemon stop → STOPPED then start → ALREADY_RUNNING). FIX: unbind/stop-gate the seed socket BEFORE acking KIND_STOPPING, OR request_stop waits for a ping-to-fail before returning. Unit: stop then immediate is_running()==false. (v0.12.0)
  [OK] REQ-HAZARD-DAEMON-STOP-REAP  required: [impl, unit]  stages: -doc +impl +unit -int
       Breap: `spt daemon stop` REAPS the spt-hosted children it spawned — no orphaned psyche/harness processes. Today a stop leaves ~8 orphaned claude-spt-psyche.exe + spt.exe: Psyches are spawned DETACHED (runtime.rs:342-356, the Child is dropped — 'Detached' ~349) and the livehost stop flag Arc<AtomicBool> is NEVER raised (brainproc.rs:227-230 holds it 'for symmetry'). FIX: on stop, raise the livehost stop flag AND kill the spawned psyche/spt-hosted children — via a Windows job object / Unix process-group so the children die with the daemon (not detached-immortal). Folds with B3 (both the stop path). (v0.12.0)
  [OK] REQ-HAZARD-DEFERRED-DRAIN  required: [impl, unit]  stages: -doc +impl +unit -int
       Deferred spool rows excluded from the event-stream drain (1.4)
  [OK] REQ-HAZARD-DEFERRED-MANIFEST  required: [impl, unit]  stages: -doc +impl +unit -int
       A pointer-mode (delegated / GhReleaseManaged) adapter whose binary/manifest is not yet extracted is reported with a CLEAR diagnostic, never silently dropped. Today such an adapter reads its manifest LIVE from source_dir (registry.rs manifest_dir ~146/149); a deferred / un-extracted install makes load_manifest fail → registered() (~410, filter_map(.ok())) SILENTLY DROPS the row → downstream ADAPTER_UNRESOLVED + a cryptic os-error-2 on `spt adapter use`. FIX: surface a clear diagnostic at the resolver + at `adapter use` (name the adapter + the deferred/missing-manifest cause + the fix), not a silent filter-drop and not a bare os-error-2; consider an eager manifest copy at register time so host_binaries survive before the binary download completes. doyle Finding A. (post-v0.10.0)
  [OK] REQ-HAZARD-DEFERRED-SURVIVE-DRAIN  required: [impl, unit]  stages: -doc +impl +unit -int
       Deferred rows survive poll drain (4.4)
  [OK] REQ-HAZARD-DETACHED-PIPE-INHERIT  required: [impl, unit]  stages: +doc +impl +unit -int
       Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)
  [OK] REQ-HAZARD-DIRECT-WRITE-PRECEDENCE  required: [impl, unit]  stages: -doc +impl +unit -int
       Direct-write precedence marker (with node id) guards stale overwrite (6.5)
  [OK] REQ-HAZARD-DROP-FILE-SINGLE-WRITER  required: [impl, unit]  stages: -doc +impl +unit -int
       Drop files are daemon-owned single-writer (6.4)
  [OK] REQ-HAZARD-EBUSY-RENAME  required: [impl, unit]  stages: -doc +impl +unit -int
       tmp-write + atomic-rename + retry on Windows EBUSY (5.2)
  [OK] REQ-HAZARD-ECHO-BEFORE-SIGNOFF  required: [impl, unit]  stages: -doc +impl +unit -int
       Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)
  [OK] REQ-HAZARD-ELEVATED-DAEMON-SPAWN  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       The daemon always runs unelevated in the invoking user's universe, regardless of which command spawns it: an elevated spawner de-elevates (Windows: UAC linked token via CreateProcessWithTokenW; Linux: drop to SUDO_UID/SUDO_GID + the invoker's HOME) — an elevated daemon's pipes deny unelevated clients (every later spt reads not-running→spawn→bind Access-denied) and a sudo'd daemon roots the user's state universe (5.7)
  [OK] REQ-HAZARD-ENV-SUBST  required: [impl, unit, int]  stages: -doc +impl +unit +int
       `spt endpoint run` HONORS manifest [env.<VAR>] direction=inject values (with {key} substitution) on the spt-hosted spawn. Today only the [session.self] command ARGV is {id}-substituted; the [env] inject value is NEITHER substituted NOR applied — manifest.schema.json promises EnvVar.value = 'Value to inject (with substitution)' but prepare_harness_spawn fills only argv and SpawnReq carries no env, so a [env.SPT_ENDPOINT_ID].value='{id}' arrives EMPTY. A FLAGLESS harness (bare `claude`, no argv slot for {id}) then routes the id via [env] → empty → SessionStart sees empty $SPT_ENDPOINT_ID → seeds-by-PPID instead of binding → ZERO perch → NO_PERCH (the actual wall-b bind blocker; perri hard-repro'd). SILENT failure (empty inject, no error). FIX (doyle ruled a): fill every [env] inject value from the SAME {key} catalog as argv/role (mirror F-009 TEMPLATE fill, whole-string fill_template for an env value), thread it through SpawnReq.env → the broker sets it on the spawned PTY child. Correctness fix — schema already promises it, NO manifest change, NO new binary. PAIRS with REQ-SEND-SPT-HOSTED to make endpoint run fully work. doyle F-013. (post-v0.10.0)
  [OK] REQ-HAZARD-ENVELOPE-CR-LINESAFE  required: [impl, unit]  stages: -doc +impl +unit -int
       Envelope CR-linesafety (4.1): the line-framed EVENT codec must neutralize raw carriage returns — `event_body_escape` folds CRLF/lone-CR to the codec's representable linebreak (`\n`→`<br>`) BEFORE framing, so a body carrying `\r` (Windows `echo`/CRLF text crossing nodes) cannot survive into the single-line envelope and trigger a receiver terminal CR→col0 overwrite that corrupts the frame. Robustness on unrepresentable input, NOT a wire-format change (decoder untouched, amp-last invariant held). Belt-and-suspenders: `spt send`/`ring` also trim stdin (parity with `notify`).
  [OK] REQ-HAZARD-ENVELOPE-DECODE-ORDER  required: [impl, unit]  stages: -doc +impl +unit -int
       Envelope decode order, ampersand decoded last (4.1)
  [OK] REQ-HAZARD-ENVELOPE-PARSER-SAFE  required: [impl, unit]  stages: -doc +impl +unit -int
       Two-slice envelope parser is panic-free and tolerant (4.2)
  [OK] REQ-HAZARD-EPHEMERAL-CLEANUP  required: [impl, unit]  stages: -doc +impl +unit -int
       Ephemeral perch cleanup on every ring exit path (3.1)
  [OK] REQ-HAZARD-EPOCH-RESET  required: []  stages: +doc -impl -unit -int
       Advertisement-epoch reset strands a node: peers' higher last-seen epoch drops the reset node's fresh advertisements as Stale until the counter outruns history. Common case (full reinstall/re-pair) is mitigated by REQ-SUBNET-7's ceremony eviction (peer-side epoch memory dies with the deleted row — acceptance-verified); the residual narrow slice (epoch file lost, identity kept) is documented, guard deferred to a field hit (4.11)
  [OK] REQ-HAZARD-EVENTPART-REASSEMBLY  required: [impl, unit]  stages: -doc +impl +unit -int
       EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently
  [OK] REQ-HAZARD-GEN-START-NOW  required: [impl, int]  stages: -doc +impl -unit +int
       gen_start = now() on cold-start and handoff (2.4)
  [OK] REQ-HAZARD-GRACE-BEFORE-SIGNOFF  required: [impl, unit]  stages: -doc +impl +unit -int
       Grace-period wait completes before composing INIT_SIGNOFF (1.1)
  [OK] REQ-HAZARD-HANDOFF-ARGV-COMPAT  required: [impl, unit]  stages: -doc +impl +unit +int
       Broker/brain IPC + handoff argv version-tolerant (2.3)
  [OK] REQ-HAZARD-HOSTED-LIVENESS-RECONCILE  required: [impl, unit, int]  stages: -doc +impl +unit +int
       B2 KEYSTONE: a daemon-hosted (spt-hosted) endpoint's info.json status is RECONCILED to real liveness, not left latched online. The broker exit-waiter (broker.rs:889-910) reaps its in-mem session table + emits ExitEvent but NEVER touches info.json; lifecycle::mark_offline only fires on Psyche teardown — so a dead/exited harness (operator closed the tab) stays status=online forever (is_perch_alive returns ONLINE for daemon-hosted, liveness.rs:80-93). FIX (doyle ruled PULL-PRIMARY — the live-status analog of REQ-HAZARD-ROSTER-GHOST): the livehost reconcile loop (reconcile_once livehost.rs:226-313) queries the broker's live session set (KIND_SESSIONS) each tick and, for any status=online live_agent perch PAST the boot grace whose endpoint has NO live broker session, marks it offline (lifecycle::mark_offline


Verify the two new REQs + exit code


109:  [OK] REQ-HAZARD-DAEMON-STOP-BARRIER  required: [impl, unit]  stages: -doc +impl +unit -int
111:  [OK] REQ-HAZARD-DAEMON-STOP-REAP  required: [impl, unit]  stages: -doc +impl +unit -int
491:EXIT=0


Both new REQs `[OK]` with `+impl +unit`, traceable `EXIT=0`, 0 findings. Now run the new units (reap + seedmap), serial, bounded:


CARGO_TARGET_DIR=target-seam cargo test -p spt-daemon --lib -- reap seedmap 2>&1 | tail -40

Run reap + seedmap lib unit tests


Command running in background with ID: bg501tw12. 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\bg501tw12.output. You will be notified when it completes. To check interim output, use Read on that file path.


ToolSearch

Question: select:Monitor


Question: select:Monitor


Awaiting the unit test result (notification will arrive). Let me read the output once it completes:


<system-reminder>Warning: the file exists but is shorter than the provided offset (1). The file has 1 lines.</system-reminder>


Test still building. I'll wait for the completion notification.


Monitor

spt-daemon reap+seedmap unit test result

until grep -qE "test result:|error\[|error:|panicked" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bg501tw12.output" 2>/dev/null; do sleep 3; done; grep -E "test result:|error\[|error:|panicked|running [0-9]+ test" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bg501tw12.output"


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


<task-notification>
<task-id>bg501tw12</task-id>
<tool-use-id>toolu_01PCqSyuQiYuWTeYJ3SpQTFb</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\bg501tw12.output</output-file>
<status>completed</status>
<summary>Background command "Run reap + seedmap lib unit tests" completed (exit code 0)</summary>
</task-notification>


<task-notification>
<task-id>bpvwxl6ik</task-id>
<summary>Monitor event: "spt-daemon reap+seedmap unit test result"</summary>
<event>running 7 tests
thread 'reap::tests::job_reaps_enrolled_child' (104352) panicked at crates\spt-daemon\src\reap.rs:251:9:
test result: FAILED. 6 passed; 1 failed; 0 ignored; 0 measured; 315 filtered out; finished in 5.05s
error: test failed, to rerun pass `-p spt-daemon --lib`</event>
</task-notification>


<task-notification>
<task-id>bpvwxl6ik</task-id>
<tool-use-id>toolu_0167RMfsGo9WavFWx8ky788p</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\bpvwxl6ik.output</output-file>
<status>completed</status>
<summary>Monitor "spt-daemon reap+seedmap unit test result" stream ended</summary>
</task-notification>


While the units run, let me re-verify the barrier logic holds no edge cases. Waiting on the monitor for the result.


237	/// How long [`request_stop`] waits for the seed socket to actually go away after
238	/// the `stopping` ack before giving up. The serve loop returns within one accept
239	/// of the stop latch (the throwaway dial wakes it), so this only ever spends a
240	/// few poll slices — the ceiling is a wedge guard, not the expected cost.
241	const STOP_BARRIER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
242	/// Poll slice for the stop barrier.
243	const STOP_BARRIER_POLL: std::time::Duration = std::time::Duration::from_millis(10);
244
245	/// Ask the daemon serving `name` to stop (graceful: it acks `stopping`, then
246	/// its serve loop returns — `spt daemon stop`, M8 decision 5 / REQ-CLI-2).
247	///
248	/// Returns only once the daemon has actually stopped answering — the **stop
249	/// barrier** (REQ-HAZARD-DAEMON-STOP-BARRIER / B3). The `stopping` ack is sent
250	/// BEFORE the serve loop drops its listener, so returning on the bare ack leaves
251	/// a window where the socket is still bound: a following `is_running` ping (the
252	/// `daemon start` liveness check) wins that window and start falsely reports
253	/// ALREADY_RUNNING. We close it by polling [`ping`] to failure after the ack —
254	/// when the probe can no longer reach a listener, the stop has fully completed
255	/// and an immediate restart is safe.
256	// [impl->REQ-CLI-2]
257	// [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]
258	pub fn request_stop(name: &str) -> io::Result<()> {
259	    let mut conn = connect(name)?;
260	    write_frame(
261	        &mut conn,
262	        &Envelope::new(KIND_STOP, serde_json::Value::Null),
263	    )?;
264	    let env = read_frame(&mut conn)?;
265	    if env.kind != KIND_STOPPING {
266	        return Err(io::Error::new(
267	            io::ErrorKind::InvalidData,
268	            "expected stopping ack",
269	        ));
270	    }
271	    drop(conn); // release our connection so it never props the loop open
272	    // Barrier: the ack is not the stop — wait until the listener is truly gone.
273	    let mut waited = std::time::Duration::ZERO;
274	    while waited < STOP_BARRIER_TIMEOUT {
275	        if ping(name).is_err() {
276	            return Ok(()); // no listener answers — stop has fully completed
277	        }
278	        std::thread::sleep(STOP_BARRIER_POLL);
279	        waited += STOP_BARRIER_POLL;
280	    }
281	    Err(io::Error::new(
282	        io::ErrorKind::TimedOut,
283	        "daemon acked stopping but kept answering past the stop barrier",
284	    ))
285	}
286

/// How long [`request_stop`] waits for the seed socket to actually go away after
/// the `stopping` ack before giving up. The serve loop returns within one accept
/// of the stop latch (the throwaway dial wakes it), so this only ever spends a
/// few poll slices — the ceiling is a wedge guard, not the expected cost.
const STOP_BARRIER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
/// Poll slice for the stop barrier.
const STOP_BARRIER_POLL: std::time::Duration = std::time::Duration::from_millis(10);

/// Ask the daemon serving `name` to stop (graceful: it acks `stopping`, then
/// its serve loop returns — `spt daemon stop`, M8 decision 5 / REQ-CLI-2).
///
/// Returns only once the daemon has actually stopped answering — the **stop
/// barrier** (REQ-HAZARD-DAEMON-STOP-BARRIER / B3). The `stopping` ack is sent
/// BEFORE the serve loop drops its listener, so returning on the bare ack leaves
/// a window where the socket is still bound: a following `is_running` ping (the
/// `daemon start` liveness check) wins that window and start falsely reports
/// ALREADY_RUNNING. We close it by polling [`ping`] to failure after the ack —
/// when the probe can no longer reach a listener, the stop has fully completed
/// and an immediate restart is safe.
// [impl->REQ-CLI-2]
// [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]
pub fn request_stop(name: &str) -> io::Result<()> {
    let mut conn = connect(name)?;
    write_frame(
        &mut conn,
        &Envelope::new(KIND_STOP, serde_json::Value::Null),
    )?;
    let env = read_frame(&mut conn)?;
    if env.kind != KIND_STOPPING {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "expected stopping ack",
        ));
    }
    drop(conn); // release our connection so it never props the loop open
    // Barrier: the ack is not the stop — wait until the listener is truly gone.
    let mut waited = std::time::Duration::ZERO;
    while waited < STOP_BARRIER_TIMEOUT {
        if ping(name).is_err() {
            return Ok(()); // no listener answers — stop has fully completed
        }
        std::thread::sleep(STOP_BARRIER_POLL);
        waited += STOP_BARRIER_POLL;
    }
    Err(io::Error::new(
        io::ErrorKind::TimedOut,
        "daemon acked stopping but kept answering past the stop barrier",
    ))
}


Prompt:
<task-notification>
<task-id>bg501tw12</task-id>
<tool-use-id>toolu_01PCqSyuQiYuWTeYJ3SpQTFb</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\bg501tw12.output</output-file>
<status>completed</status>
<summary>Background command "Run reap + seedmap lib unit tests" completed (exit code 0)</summary>
</task-notification>


Prompt:
<task-notification>
<task-id>bpvwxl6ik</task-id>
<summary>Monitor event: "spt-daemon reap+seedmap unit test result"</summary>
<event>running 7 tests
thread 'reap::tests::job_reaps_enrolled_child' (104352) panicked at crates\spt-daemon\src\reap.rs:251:9:
test result: FAILED. 6 passed; 1 failed; 0 ignored; 0 measured; 315 filtered out; finished in 5.05s
error: test failed, to rerun pass `-p spt-daemon --lib`</event>
</task-notification>


Prompt:
<task-notification>
<task-id>bpvwxl6ik</task-id>
<tool-use-id>toolu_0167RMfsGo9WavFWx8ky788p</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\bpvwxl6ik.output</output-file>
<status>completed</status>
<summary>Monitor "spt-daemon reap+seedmap unit test result" stream ended</summary>
</task-notification>


Monitor shows `job_reaps_enrolled_child` FAILED (the `tail` pipe masked the real exit code). Read the panic detail:


Bash

Read full unit-test output


   Compiling spt-daemon v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 12.72s
     Running unittests src\lib.rs (target-seam\debug\deps\spt_daemon-93d5bbc8c8692bc0.exe)

running 7 tests
test reap::tests::reap_without_enroll_is_noop ... ok
test seedmap::tests::stop_op_acks_then_serve_returns ... ok
test seedmap::tests::put_then_take_consumes_once ... ok
test seedmap::tests::request_stop_barrier_holds_until_no_listener ... ok
test seedmap::tests::put_refreshes_existing ... ok
test seedmap::tests::take_absent_is_none ... ok
test reap::tests::job_reaps_enrolled_child ... FAILED

failures:

---- reap::tests::job_reaps_enrolled_child stdout ----

thread 'reap::tests::job_reaps_enrolled_child' (104352) panicked at crates\spt-daemon\src\reap.rs:251:9:
reap must terminate the enrolled child
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    reap::tests::job_reaps_enrolled_child

test result: FAILED. 6 passed; 1 failed; 0 ignored; 0 measured; 315 filtered out; finished in 5.05s

error: test failed, to rerun pass `-p spt-daemon --lib`


<task-notification>
<task-id>bc3ve4fjf</task-id>
<tool-use-id>toolu_01SFK93pTUbAXHd5kKhybUW3</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\bc3ve4fjf.output</output-file>
<status>completed</status>
<summary>Background command "Re-run reap test with nocapture to see job diagnostics" completed (exit code 0)</summary>
</task-notification>


B3 barrier unit **passed**. The reap noop passed. `job_reaps_enrolled_child` failed — `TerminateJobObject` didn't kill the child. Likely the test runner is already inside a Job (cargo harness) and `AssignProcessToJobObject` to my new job failed. Diagnose with `--nocapture` to see the `REAP_JOB_*` eprintln:


CARGO_TARGET_DIR=target-seam cargo test -p spt-daemon --lib -- --nocapture job_reaps_enrolled_child 2>&1 | grep -iE "REAP_JOB|panicked|test result|running [0-9]"

Re-run reap test with nocapture to see job diagnostics


Command running in background with ID: bc3ve4fjf. 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\bc3ve4fjf.output. You will be notified when it completes. To check interim output, use Read on that file path.


until grep -qiE "test result|panicked" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bc3ve4fjf.output" 2>/dev/null; do sleep 3; done; cat "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bc3ve4fjf.output"

Wait for and read nocapture diagnostics


Command running in background with ID: bp41kc427. 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\bp41kc427.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bp41kc427</task-id>
<tool-use-id>toolu_01HZXmpScMmvRq3HLkYk7e4j</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\bp41kc427.output</output-file>
<status>completed</status>
<summary>Background command "Wait for and read nocapture diagnostics" completed (exit code 0)</summary>
</task-notification>


Prompt:
<task-notification>
<task-id>bc3ve4fjf</task-id>
<tool-use-id>toolu_01SFK93pTUbAXHd5kKhybUW3</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\bc3ve4fjf.output</output-file>
<status>completed</status>
<summary>Background command "Re-run reap test with nocapture to see job diagnostics" completed (exit code 0)</summary>
</task-notification>


Read nocapture diagnostics


running 1 test
thread 'reap::tests::job_reaps_enrolled_child' (162428) panicked at crates\spt-daemon\src\reap.rs:251:9:
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 321 filtered out; finished in 5.04s


Prompt:
<task-notification>
<task-id>bp41kc427</task-id>
<tool-use-id>toolu_01HZXmpScMmvRq3HLkYk7e4j</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\bp41kc427.output</output-file>
<status>completed</status>
<summary>Background command "Wait for and read nocapture diagnostics" completed (exit code 0)</summary>
</task-notification>


No `REAP_JOB_*` errors → all three Win32 calls succeeded; the reap *did* run. Root cause is the **test's liveness check**, not the reap: on Windows a `TerminateJobObject`'d process whose handle is still held (the test's `Child`) keeps a live kernel object, so `OpenProcess`/`is_process_alive` still reads "alive". The correct check for an owned child is `try_wait()`. Same fix serves the Unix zombie case. Rewrite the test helper:


Edit

#[cfg(test)]
mod tests {
    use super::*;
    use spt_store::proc::is_process_alive;
    use std::process::Command;
    use std::time::Duration;

    /// Poll until `pid` reads dead (zombie counts as dead — `is_process_alive`
    /// handles it) or the bound elapses.
    fn wait_dead(pid: u32) -> bool {
        for _ in 0..250 {
            if !is_process_alive(pid) {
                return true;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        false
    }

    // [unit->REQ-HAZARD-DAEMON-STOP-REAP] an enrolled child is terminated by the
    // reap — the mechanism that makes the brain (and the Psyches it spawns, which
    // inherit the same job/group) die with the daemon on stop instead of
    // orphaning. The child is a long-lived sleeper rooted in its own container;
    // reap() must kill it.
    #[cfg(windows)]
    #[test]
    fn job_reaps_enrolled_child() {
        let reaper = BrainReaper::new();
        // A real long-running child (ping's -n is a 1s/iteration sleeper).
        let mut child = Command::new("cmd")
            .args(["/C", "ping -n 60 127.0.0.1 >NUL"])
            .spawn()
            .expect("spawn sleeper");
        reaper.enroll(&child);
        let pid = child.id();
        assert!(is_process_alive(pid), "enrolled child starts alive");
        reaper.reap();
        assert!(wait_dead(pid), "reap must terminate the enrolled child");
        let _ = child.wait();
    }

    #[cfg(unix)]
    #[test]
    fn group_reaps_enrolled_child() {
        use std::os::unix::process::CommandExt;
        let reaper = BrainReaper::new();
        let mut cmd = Command::new("sh");
        cmd.args(["-c", "sleep 60"]);
        // Mirror the production brain spawn: the child leads its OWN group so the
        // reap (kill -pgid) hits the child subtree, never the test runner.
        unsafe {
            cmd.pre_exec(|| {
                libc::setpgid(0, 0);
                Ok(())
            });
        }
        let mut child = cmd.spawn().expect("spawn sleeper");
        reaper.enroll(&child);
        let pid = child.id();
        assert!(is_process_alive(pid), "enrolled child starts alive");
        reaper.reap();
        assert!(wait_dead(pid), "reap must terminate the enrolled child's group");
        let _ = child.wait();
    }

#[cfg(test)]
mod tests {
    use super::*;
    use std::process::{Child, Command};
    use std::time::Duration;

    /// Poll until our OWN `child` is observed exited, or the bound elapses. We
    /// hold the `Child` handle, so `try_wait` is the correct liveness probe:
    /// `OpenProcess`-based checks read a Windows process we still hold a handle to
    /// as "alive" even after `TerminateJobObject`, and on Unix the killed child is
    /// a zombie until reaped — `try_wait` resolves both (and reaps the zombie).
    fn wait_exit(child: &mut Child) -> bool {
        for _ in 0..250 {
            if matches!(child.try_wait(), Ok(Some(_))) {
                return true;
            }
            std::thread::sleep(Duration::from_millis(20));
        }
        false
    }

    // [unit->REQ-HAZARD-DAEMON-STOP-REAP] an enrolled child is terminated by the
    // reap — the mechanism that makes the brain (and the Psyches it spawns, which
    // inherit the same job/group) die with the daemon on stop instead of
    // orphaning. The child is a long-lived sleeper rooted in its own container;
    // reap() must kill it.
    #[cfg(windows)]
    #[test]
    fn job_reaps_enrolled_child() {
        let reaper = BrainReaper::new();
        // A real long-running child (ping's -n is a 1s/iteration sleeper).
        let mut child = Command::new("cmd")
            .args(["/C", "ping -n 60 127.0.0.1 >NUL"])
            .spawn()
            .expect("spawn sleeper");
        reaper.enroll(&child);
        assert!(
            matches!(child.try_wait(), Ok(None)),
            "enrolled child starts alive"
        );
        reaper.reap();
        assert!(wait_exit(&mut child), "reap must terminate the enrolled child");
    }

    #[cfg(unix)]
    #[test]
    fn group_reaps_enrolled_child() {
        use std::os::unix::process::CommandExt;
        let reaper = BrainReaper::new();
        let mut cmd = Command::new("sh");
        cmd.args(["-c", "sleep 60"]);
        // Mirror the production brain spawn: the child leads its OWN group so the
        // reap (kill -pgid) hits the child subtree, never the test runner.
        unsafe {
            cmd.pre_exec(|| {
                libc::setpgid(0, 0);
                Ok(())
            });
        }
        let mut child = cmd.spawn().expect("spawn sleeper");
        reaper.enroll(&child);
        assert!(
            matches!(child.try_wait(), Ok(None)),
            "enrolled child starts alive"
        );
        reaper.reap();
        assert!(
            wait_exit(&mut child),
            "reap must terminate the enrolled child's group"
        );
    }