//! The daemon process entrypoint + auto-start (B6).
//!
//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds
//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the
//! canonical per-home names ([`crate::endpoint`]) and serves both for the life
//! of the process.
//!
//! [`ensure_running`] is the auto-start seam (REQ-DAEMON-3): any `api`
//! invocation calls it; if no daemon answers the seed-control ping it spawns
//! `spt daemon` detached and waits (bounded) for it to come up. The reliable
//! anchor is the seed channel — bound for the whole daemon lifetime, so a ping
//! against it is a true liveness test (`listen` is the interim model's anchor;
//! here the seed channel plays that role for cold-start detection).
// [impl->REQ-DAEMON-3]

use std::io;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use crate::broker::Broker;
use crate::endpoint::{broker_socket_name, daemon_pid_path, seed_socket_name};
use crate::seedmap::{self, SeedRegistry};

/// How long auto-start waits for a freshly-spawned daemon to bind its channel.
const START_TIMEOUT: Duration = Duration::from_secs(5);
/// Poll cadence while waiting for the spawned daemon to come up.
const START_POLL: Duration = Duration::from_millis(25);

/// The per-machine broker/brain daemon process.
pub struct Daemon;

impl Daemon {
    /// Run the daemon: bind the broker (served on a background thread) + the
    /// seed-control channel (the foreground loop + liveness anchor) and serve
    /// until the process is killed. The body of `spt daemon`.
    pub fn run() -> io::Result<()> {
        let reg = Arc::new(SeedRegistry::new());

        // The broker hosts PTYs (B1/B2). Serve it on a background thread; a bind
        // failure there must not stop the seed channel (the B6 surface) — the
        // PTY-hosting consumers arrive in B7/B8.
        match Broker::bind(&broker_socket_name()) {
            Ok(broker) => {
                thread::spawn(move || {
                    let _ = broker.serve();
                });
            }
            Err(e) => eprintln!("BROKER_BIND_FAIL: {e}"),
        }

        // Best-effort breadcrumb: record our pid for management/cleanup. Liveness
        // is the socket ping, never this file, so a stale value is harmless.
        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());

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

/// Whether a daemon is already serving the seed-control channel for this home.
pub fn is_running() -> bool {
    seedmap::ping(&seed_socket_name()).is_ok()
}

/// Auto-start (REQ-DAEMON-3): ensure a daemon is up for this home. A no-op if one
/// already answers; otherwise spawn `spt daemon` detached and wait (bounded) for
/// it to bind. Idempotent + race-tolerant: if two callers spawn at once, one
/// wins the bind and the other exits — and this check passes either way.
pub fn ensure_running() -> io::Result<()> {
    if is_running() {
        return Ok(());
    }
    spawn_detached()?;
    let mut waited = Duration::ZERO;
    while waited < START_TIMEOUT {
        if is_running() {
            return Ok(());
        }
        thread::sleep(START_POLL);
        waited += START_POLL;
    }
    Err(io::Error::new(
        io::ErrorKind::TimedOut,
        "daemon did not come up within the start window",
    ))
}

/// Spawn `spt daemon` as a detached background process. Inherits the environment
/// — so the child resolves the **same** `SPT_HOME`, hence the same canonical
/// socket names — but drops the std handles so it outlives the launching CLI
/// call. The child is independent (not killed when the parent exits): on unix it
/// is reparented to init when the launcher exits; on Windows a child is not in
/// the parent's job by default.
fn spawn_detached() -> io::Result<()> {
    let exe = std::env::current_exe()?;
    Command::new(exe)
        .arg("daemon")
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::seedmap::{serve_seed_control, SeedRegistry};
    use crate::test_home::with_home;

    // [unit->REQ-DAEMON-3] with no daemon up, is_running() is false; once a
    // seed-control server answers, it flips true and ensure_running() is a no-op
    // (the spawn branch — which would need the real `spt` binary — is not taken).
    #[test]
    fn is_running_tracks_the_seed_channel() {
        with_home(|_| {
            assert!(!is_running(), "no daemon bound yet");

            let reg = Arc::new(SeedRegistry::new());
            let name = seed_socket_name();
            let srv = Arc::clone(&reg);
            thread::spawn(move || {
                let _ = serve_seed_control(&name, &srv);
            });
            for _ in 0..400 {
                if is_running() {
                    break;
                }
                thread::sleep(Duration::from_millis(5));
            }
            assert!(is_running(), "the seed channel answers → daemon is up");
            // ensure_running short-circuits (no real process spawn).
            ensure_running().expect("ensure is a no-op when already up");
        });
    }
}


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

//! The daemon process entrypoint + auto-start (B6).
//!
//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds
//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the
//! canonical per-home names ([`crate::endpoint`]) and serves both for the life
//! of the process.
//!
//! [`ensure_running`] is the auto-start seam (REQ-DAEMON-3): any `api`
//! invocation calls it; if no daemon answers the seed-control ping it spawns
//! `spt daemon` detached and waits (bounded) for it to come up. The reliable
//! anchor is the seed channel — bound for the whole daemon lifetime, so a ping
//! against it is a true liveness test (`listen` is the interim model's anchor;
//! here the seed channel plays that role for cold-start detection).
// [impl->REQ-DAEMON-3]

use std::io;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use crate::broker::Broker;
use crate::endpoint::{broker_socket_name, daemon_pid_path, seed_socket_name};
use crate::seedmap::{self, SeedRegistry};

/// How long auto-start waits for a freshly-spawned daemon to bind its channel.
const START_TIMEOUT: Duration = Duration::from_secs(5);
/// Poll cadence while waiting for the spawned daemon to come up.
const START_POLL: Duration = Duration::from_millis(25);

/// The per-machine broker/brain daemon process.
pub struct Daemon;

impl Daemon {
    /// Run the daemon: bind the broker (served on a background thread) + the
    /// seed-control channel (the foreground loop + liveness anchor) and serve
    /// until the process is killed. The body of `spt daemon`.
    pub fn run() -> io::Result<()> {
        let reg = Arc::new(SeedRegistry::new());

        // The broker hosts PTYs (B1/B2). Serve it on a background thread; a bind
        // failure there must not stop the seed channel (the B6 surface) — the
        // PTY-hosting consumers arrive in B7/B8.
        match Broker::bind(&broker_socket_name()) {
            Ok(broker) => {
                thread::spawn(move || {
                    let _ = broker.serve();
                });
            }
            Err(e) => eprintln!("BROKER_BIND_FAIL: {e}"),
        }

        // Best-effort breadcrumb: record our pid for management/cleanup. Liveness
        // is the socket ping, never this file, so a stale value is harmless.
        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());

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

/// Whether a daemon is already serving the seed-control channel for this home.
pub fn is_running() -> bool {
    seedmap::ping(&seed_socket_name()).is_ok()
}

/// Auto-start (REQ-DAEMON-3): ensure a daemon is up for this home. A no-op if one
/// already answers; otherwise spawn `spt daemon` detached and wait (bounded) for
/// it to bind. Idempotent + race-tolerant: if two callers spawn at once, one
/// wins the bind and the other exits — and this check passes either way.
pub fn ensure_running() -> io::Result<()> {
    if is_running() {
        return Ok(());
    }
    spawn_detached()?;
    let mut waited = Duration::ZERO;
    while waited < START_TIMEOUT {
        if is_running() {
            return Ok(());
        }
        thread::sleep(START_POLL);
        waited += START_POLL;
    }
    Err(io::Error::new(
        io::ErrorKind::TimedOut,
        "daemon did not come up within the start window",
    ))
}

/// Spawn `spt daemon` as a detached background process. Inherits the environment
/// — so the child resolves the **same** `SPT_HOME`, hence the same canonical
/// socket names — but drops the std handles so it outlives the launching CLI
/// call. The child is independent (not killed when the parent exits): on unix it
/// is reparented to init when the launcher exits; on Windows a child is not in
/// the parent's job by default.
fn spawn_detached() -> io::Result<()> {
    let exe = std::env::current_exe()?;
    Command::new(exe)
        .arg("daemon")
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::seedmap::{serve_seed_control, SeedRegistry};
    use crate::test_home::with_home;

    // [unit->REQ-DAEMON-3] with no daemon up, is_running() is false; once a
    // seed-control server answers, it flips true and ensure_running() is a no-op
    // (the spawn branch — which would need the real `spt` binary — is not taken).
    #[test]
    fn is_running_tracks_the_seed_channel() {
        with_home(|_| {
            assert!(!is_running(), "no daemon bound yet");

            let reg = Arc::new(SeedRegistry::new());
            let name = seed_socket_name();
            let srv = Arc::clone(&reg);
            thread::spawn(move || {
                let _ = serve_seed_control(&name, &srv);
            });
            for _ in 0..400 {
                if is_running() {
                    break;
                }
                thread::sleep(Duration::from_millis(5));
            }
            assert!(is_running(), "the seed channel answers → daemon is up");
            // ensure_running short-circuits (no real process spawn).
            ensure_running().expect("ensure is a no-op when already up");
        });
    }
}


Now wire lib.rs:


Edit

pub mod brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod effect;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod relay;
pub mod transport;

#[cfg(test)]
mod test_home;

pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::DaemonConfig;
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use lifecycle::{BrainLifecycle, TickReport};
pub use relay::Relay;

pub mod brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod daemon;
pub mod effect;
pub mod endpoint;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod relay;
pub mod seedmap;
pub mod transport;

#[cfg(test)]
mod test_home;

pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::DaemonConfig;
pub use daemon::{ensure_running, is_running, Daemon};
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use endpoint::{broker_socket_name, daemon_pid_path, seed_socket_name};
pub use lifecycle::{BrainLifecycle, TickReport};
pub use relay::Relay;
pub use seedmap::{put_seed, take_seed, SeedRegistry};


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 brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod daemon;
pub mod effect;
pub mod endpoint;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod relay;
pub mod seedmap;
pub mod transport;

#[cfg(test)]
mod test_home;

pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::DaemonConfig;
pub use daemon::{ensure_running, is_running, Daemon};
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use endpoint::{broker_socket_name, daemon_pid_path, seed_socket_name};
pub use lifecycle::{BrainLifecycle, TickReport};
pub use relay::Relay;
pub use seedmap::{put_seed, take_seed, SeedRegistry};

//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).
//!
//! ADR-0004 splits the logical daemon into two implementation layers so a
//! routine self-update swaps the logic without disturbing live endpoints:
//!
//! - a **broker** (stable kernel) holds *only* the un-transferable resources a
//!   live consumer would lose continuity on if the logic restarted — PTY
//!   masters (from [`spt_term`]), spawned harness children, accepted/listening
//!   sockets — and runs no logic;
//! - a **brain** (restartable logic) holds routing, registry, manifest parse,
//!   and the re-hosted [`spt_live`] lifecycle, attaching to the broker over a
//!   **versioned local IPC** so a *newer brain talks to an older broker*.
//!
//! **B0 (this commit)** lands the crate's layer position plus that versioned
//! IPC seam — the load-bearing contract everything else rides:
//!
//! - [`frame`] — the forward-compatible [`Envelope`] schema + the version
//!   [`Hello`] handshake (KNOWN-HAZARDS 2.3, [`REQ-HAZARD-HANDOFF-ARGV-COMPAT`]);
//! - [`codec`] — length-prefixed JSON framing over any [`std::io::Read`]/`Write`;
//! - [`transport`] — the OS-split carrier (named pipe / UDS) behind one
//!   [`DaemonTransport`] trait, mirroring `spt-term`'s `SessionSurface`.
//!
//! **B1 (this commit)** adds the **broker kernel** ([`broker`]) — the stable
//! half that hosts a real `spt-term` PTY child and relays its output/input over
//! the IPC, holding only the ADR-0004 §B resources and no logic — plus the
//! broker↔brain message catalog ([`msg`]).
//!
//! **B3 (this commit)** consolidates the interim no-daemon model into the brain:
//! the `api listen` pulse / Psyche / drop-ingest loops become scheduled
//! in-process loops ([`lifecycle::BrainLifecycle`]) paced by a durable,
//! configurable period ([`config::DaemonConfig`]) instead of the interim 5s
//! constant; the only residue left in the harness-hosted session is the thin,
//! stateless [`relay::Relay`]. The `spt` binary's `LiveHost` becomes a shim over
//! `BrainLifecycle` (re-host, not rewrite) ([`REQ-DAEMON-1`]).
//!
//! **B5 (this commit)** makes the broker↔brain boundary **exactly-once across a
//! brain crash** ([`effect::EffectJournal`]): an idempotent `input` carries a
//! durable `op_id`, and the broker dedups at the effect keyed by that id over a
//! broker-owned recovery anchor (KNOWN-HAZARDS 7.2, [`REQ-HAZARD-RESTART-IDEMPOTENT`];
//! Spike #6's two constraints — broker-owned anchor, dedup-at-effect-by-id —
//! instantiated on the real PTY-write boundary).
//!
//! The brain + gapless handoff (B2) and the rest of the M3b task table build on
//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK
//! (R-ARCH-2).
//!
//! [`Envelope`]: frame::Envelope
//! [`Hello`]: frame::Hello
//! [`DaemonTransport`]: transport::DaemonTransport

pub mod brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod effect;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod relay;
pub mod transport;

#[cfg(test)]
mod test_home;

pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::DaemonConfig;
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use lifecycle::{BrainLifecycle, TickReport};
pub use relay::Relay;
pub use frame::{
    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
    MIN_COMPATIBLE_VERSION,
};
pub use transport::{DaemonTransport, LocalSocketTransport};

pub mod broker;
pub mod codec;
pub mod config;
+pub mod daemon;
pub mod effect;
+pub mod endpoint;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod relay;
+pub mod seedmap;
pub mod transport;
#[cfg(test)]

+pub use daemon::{ensure_running, is_running, Daemon};

+pub use endpoint::{broker_socket_name, daemon_pid_path, seed_socket_name};

+pub use seedmap::{put_seed, take_seed, SeedRegistry};


NOTE FROM THE USER: proceed with nextsteps.


spt-daemon modules in. Build the crate to catch errors early.


Bash

cargo build -p spt-daemon 2>&1 | tail -30


   Compiling spt-daemon v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.03s


Compiles. Now retire the seed file bridge in spt-store.


Edit

//! Ephemeral harness-hosted startup seed — the **interim no-daemon** bridge
//! (decision 2026-06-01). A `SessionStart`-equivalent hook fires
//! `api seed --pid <parent_pid> --session-id <sid>`, writing a seed file keyed
//! by the stable parent pid. `api listen <id>` later consumes it (matched by
//! the listener's *own* parent pid), then binds the perch.
//!
//! CONTEXT describes the M3 daemon holding this in-memory; M2a mirrors M1's
//! standalone model with a file instead. **This module is the localized swap
//! point** — M3 replaces the file read/write with the daemon's in-memory map
//! and nothing else in the `api listen` path changes.
//
// [impl->REQ-START-1]

use serde::{Deserialize, Serialize};
use std::io;
use std::path::PathBuf;

use crate::atomic::atomic_write_string;
use crate::perch::spt_home;

/// One ephemeral seed entry: the session details the spt-hosted topology would
/// share directly, minus the not-yet-chosen `live_id`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Seed {
    /// The stable session-binding anchor (the harness process pid).
    pub parent_pid: u32,
    /// The harness session id — the pid-recycle discriminator (5.1).
    pub session_id: String,
    /// adapter_name that wrote the seed.
    pub adapter: String,
    /// Optional working directory the session runs in.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /// Creation stamp (opaque producer string).
    pub created: String,
}

/// `<spt_home>/seeds/`.
pub fn seeds_dir() -> PathBuf {
    spt_home().join("seeds")
}

/// `<spt_home>/seeds/<parent_pid>.json`.
pub fn seed_path(parent_pid: u32) -> PathBuf {
    seeds_dir().join(format!("{parent_pid}.json"))
}

/// Write (or refresh) the seed keyed by its `parent_pid`. Atomic (tmp+rename).
pub fn write_seed(seed: &Seed) -> io::Result<()> {
    std::fs::create_dir_all(seeds_dir())?;
    let json = serde_json::to_string(seed)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    atomic_write_string(&seed_path(seed.parent_pid), &json)
}

/// Read the seed for `parent_pid`, if present and parseable.
pub fn read_seed(parent_pid: u32) -> Option<Seed> {
    let content = std::fs::read_to_string(seed_path(parent_pid)).ok()?;
    serde_json::from_str(&content).ok()
}

/// Remove a consumed seed (best-effort; seeds are ephemeral).
pub fn remove_seed(parent_pid: u32) {
    let _ = std::fs::remove_file(seed_path(parent_pid));
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Mutex;

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    struct Home {
        _lock: std::sync::MutexGuard<'static, ()>,
        _tmp: tempfile::TempDir,
        prev: Option<String>,
    }
    impl Home {
        fn set() -> Self {
            let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
            let tmp = tempfile::tempdir().unwrap();
            let prev = std::env::var("SPT_HOME").ok();
            std::env::set_var("SPT_HOME", tmp.path());
            Home { _lock: lock, _tmp: tmp, prev }
        }
    }
    impl Drop for Home {
        fn drop(&mut self) {
            match &self.prev {
                Some(v) => std::env::set_var("SPT_HOME", v),
                None => std::env::remove_var("SPT_HOME"),
            }
        }
    }

    // [unit->REQ-START-1] a written seed reads back identically, keyed by pid.
    #[test]
    fn seed_round_trips() {
        let _h = Home::set();
        let seed = Seed {
            parent_pid: 4321,
            session_id: "sid-xyz".into(),
            adapter: "mock".into(),
            cwd: Some("C:/repo".into()),
            created: "1700000000".into(),
        };
        write_seed(&seed).unwrap();
        assert_eq!(read_seed(4321), Some(seed));
    }

    // [unit->REQ-START-1] absent seed reads None; remove clears it.
    #[test]
    fn missing_seed_is_none_and_remove_clears() {
        let _h = Home::set();
        assert_eq!(read_seed(999), None);
        let seed = Seed {
            parent_pid: 999,
            session_id: "s".into(),
            adapter: "mock".into(),
            cwd: None,
            created: "0".into(),
        };
        write_seed(&seed).unwrap();
        assert!(read_seed(999).is_some());
        remove_seed(999);
        assert_eq!(read_seed(999), None);
    }
}

//! The ephemeral harness-hosted startup [`Seed`] — the api-bridging data the
//! harness hands spt-core at `SessionStart`. A `SessionStart`-equivalent hook
//! fires `api seed --pid <parent_pid> --session-id <sid>`; a later
//! `api listen <id>` consumes it (matched by the listener's *own* parent pid),
//! then binds the perch.
//!
//! **The transport is the daemon's in-memory map, not a file (M3b / B6).** The
//! interim no-daemon model (M1/M2a) bridged the two `api` invocations through a
//! seed *file* under `<spt_home>/seeds/`; M3b retires that file bridge
//! (REQ-START-3). The seed now lives in the per-machine daemon's memory —
//! `api seed` PUTs it and `api listen` TAKEs it over the seed-control IPC
//! ([`spt_daemon::seedmap`]). This module keeps only the **shared data
//! contract** ([`Seed`]); the put/take/consume mechanics live in the daemon.
//! No seed file is ever written.
//
// [impl->REQ-START-1]

use serde::{Deserialize, Serialize};

/// One ephemeral seed entry: the session details the spt-hosted topology would
/// share directly, minus the not-yet-chosen `live_id`. The harness fills it at
/// `api seed`; the daemon holds it in memory keyed by [`Seed::parent_pid`] until
/// `api listen` consumes it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Seed {
    /// The stable session-binding anchor (the harness process pid).
    pub parent_pid: u32,
    /// The harness session id — the pid-recycle discriminator (5.1).
    pub session_id: String,
    /// adapter_name that wrote the seed.
    pub adapter: String,
    /// Optional working directory the session runs in.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /// Creation stamp (opaque producer string).
    pub created: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    // [unit->REQ-START-1] the seed data contract round-trips through serde — the
    // shape the harness writes at `api seed` and the daemon hands back at
    // `api listen` is stable across the JSON the seed-control IPC carries.
    #[test]
    fn seed_round_trips_through_json() {
        let seed = Seed {
            parent_pid: 4321,
            session_id: "sid-xyz".into(),
            adapter: "mock".into(),
            cwd: Some("C:/repo".into()),
            created: "1700000000".into(),
        };
        let json = serde_json::to_string(&seed).unwrap();
        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);
    }

    // [unit->REQ-START-1] an absent cwd is omitted on the wire (skip_serializing)
    // and decodes back to None — the optional working dir stays optional.
    #[test]
    fn absent_cwd_is_omitted() {
        let seed = Seed {
            parent_pid: 999,
            session_id: "s".into(),
            adapter: "mock".into(),
            cwd: None,
            created: "0".into(),
        };
        let json = serde_json::to_string(&seed).unwrap();
        assert!(!json.contains("cwd"), "None cwd is skipped: {json}");
        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);
    }
}


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

//! Ephemeral harness-hosted startup seed — the **interim no-daemon** bridge
//! (decision 2026-06-01). A `SessionStart`-equivalent hook fires
//! `api seed --pid <parent_pid> --session-id <sid>`, writing a seed file keyed
//! by the stable parent pid. `api listen <id>` later consumes it (matched by
//! the listener's *own* parent pid), then binds the perch.
//!
//! CONTEXT describes the M3 daemon holding this in-memory; M2a mirrors M1's
//! standalone model with a file instead. **This module is the localized swap
//! point** — M3 replaces the file read/write with the daemon's in-memory map
//! and nothing else in the `api listen` path changes.
//
// [impl->REQ-START-1]

use serde::{Deserialize, Serialize};
use std::io;
use std::path::PathBuf;

use crate::atomic::atomic_write_string;
use crate::perch::spt_home;

/// One ephemeral seed entry: the session details the spt-hosted topology would
/// share directly, minus the not-yet-chosen `live_id`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Seed {
    /// The stable session-binding anchor (the harness process pid).
    pub parent_pid: u32,
    /// The harness session id — the pid-recycle discriminator (5.1).
    pub session_id: String,
    /// adapter_name that wrote the seed.
    pub adapter: String,
    /// Optional working directory the session runs in.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /// Creation stamp (opaque producer string).
    pub created: String,
}

/// `<spt_home>/seeds/`.
pub fn seeds_dir() -> PathBuf {
    spt_home().join("seeds")
}

/// `<spt_home>/seeds/<parent_pid>.json`.
pub fn seed_path(parent_pid: u32) -> PathBuf {
    seeds_dir().join(format!("{parent_pid}.json"))
}

/// Write (or refresh) the seed keyed by its `parent_pid`. Atomic (tmp+rename).
pub fn write_seed(seed: &Seed) -> io::Result<()> {
    std::fs::create_dir_all(seeds_dir())?;
    let json = serde_json::to_string(seed)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    atomic_write_string(&seed_path(seed.parent_pid), &json)
}

/// Read the seed for `parent_pid`, if present and parseable.
pub fn read_seed(parent_pid: u32) -> Option<Seed> {
    let content = std::fs::read_to_string(seed_path(parent_pid)).ok()?;
    serde_json::from_str(&content).ok()
}

/// Remove a consumed seed (best-effort; seeds are ephemeral).
pub fn remove_seed(parent_pid: u32) {
    let _ = std::fs::remove_file(seed_path(parent_pid));
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Mutex;

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    struct Home {
        _lock: std::sync::MutexGuard<'static, ()>,
        _tmp: tempfile::TempDir,
        prev: Option<String>,
    }
    impl Home {
        fn set() -> Self {
            let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
            let tmp = tempfile::tempdir().unwrap();
            let prev = std::env::var("SPT_HOME").ok();
            std::env::set_var("SPT_HOME", tmp.path());
            Home { _lock: lock, _tmp: tmp, prev }
        }
    }
    impl Drop for Home {
        fn drop(&mut self) {
            match &self.prev {
                Some(v) => std::env::set_var("SPT_HOME", v),
                None => std::env::remove_var("SPT_HOME"),
            }
        }
    }

    // [unit->REQ-START-1] a written seed reads back identically, keyed by pid.
    #[test]
    fn seed_round_trips() {
        let _h = Home::set();
        let seed = Seed {
            parent_pid: 4321,
            session_id: "sid-xyz".into(),
            adapter: "mock".into(),
            cwd: Some("C:/repo".into()),
            created: "1700000000".into(),
        };
        write_seed(&seed).unwrap();
        assert_eq!(read_seed(4321), Some(seed));
    }

    // [unit->REQ-START-1] absent seed reads None; remove clears it.
    #[test]
    fn missing_seed_is_none_and_remove_clears() {
        let _h = Home::set();
        assert_eq!(read_seed(999), None);
        let seed = Seed {
            parent_pid: 999,
            session_id: "s".into(),
            adapter: "mock".into(),
            cwd: None,
            created: "0".into(),
        };
        write_seed(&seed).unwrap();
        assert!(read_seed(999).is_some());
        remove_seed(999);
        assert_eq!(read_seed(999), None);
    }
}

//! The ephemeral harness-hosted startup [`Seed`] — the api-bridging data the
//! harness hands spt-core at `SessionStart`. A `SessionStart`-equivalent hook
//! fires `api seed --pid <parent_pid> --session-id <sid>`; a later
//! `api listen <id>` consumes it (matched by the listener's *own* parent pid),
//! then binds the perch.
//!
//! **The transport is the daemon's in-memory map, not a file (M3b / B6).** The
//! interim no-daemon model (M1/M2a) bridged the two `api` invocations through a
//! seed *file* under `<spt_home>/seeds/`; M3b retires that file bridge
//! (REQ-START-3). The seed now lives in the per-machine daemon's memory —
//! `api seed` PUTs it and `api listen` TAKEs it over the seed-control IPC
//! ([`spt_daemon::seedmap`]). This module keeps only the **shared data
//! contract** ([`Seed`]); the put/take/consume mechanics live in the daemon.
//! No seed file is ever written.
//
// [impl->REQ-START-1]

use serde::{Deserialize, Serialize};

/// One ephemeral seed entry: the session details the spt-hosted topology would
/// share directly, minus the not-yet-chosen `live_id`. The harness fills it at
/// `api seed`; the daemon holds it in memory keyed by [`Seed::parent_pid`] until
/// `api listen` consumes it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Seed {
    /// The stable session-binding anchor (the harness process pid).
    pub parent_pid: u32,
    /// The harness session id — the pid-recycle discriminator (5.1).
    pub session_id: String,
    /// adapter_name that wrote the seed.
    pub adapter: String,
    /// Optional working directory the session runs in.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /// Creation stamp (opaque producer string).
    pub created: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    // [unit->REQ-START-1] the seed data contract round-trips through serde — the
    // shape the harness writes at `api seed` and the daemon hands back at
    // `api listen` is stable across the JSON the seed-control IPC carries.
    #[test]
    fn seed_round_trips_through_json() {
        let seed = Seed {
            parent_pid: 4321,
            session_id: "sid-xyz".into(),
            adapter: "mock".into(),
            cwd: Some("C:/repo".into()),
            created: "1700000000".into(),
        };
        let json = serde_json::to_string(&seed).unwrap();
        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);
    }

    // [unit->REQ-START-1] an absent cwd is omitted on the wire (skip_serializing)
    // and decodes back to None — the optional working dir stays optional.
    #[test]
    fn absent_cwd_is_omitted() {
        let seed = Seed {
            parent_pid: 999,
            session_id: "s".into(),
            adapter: "mock".into(),
            cwd: None,
            created: "0".into(),
        };
        let json = serde_json::to_string(&seed).unwrap();
        assert!(!json.contains("cwd"), "None cwd is skipped: {json}");
        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);
    }
}

-//! Ephemeral harness-hosted startup seed — the **interim no-daemon** bridge

-//! (decision 2026-06-01). A `SessionStart`-equivalent hook fires

-//! `api seed --pid <parent_pid> --session-id <sid>`, writing a seed file keyed

-//! by the stable parent pid. `api listen <id>` later consumes it (matched by

-//! the listener's *own* parent pid), then binds the perch.

+//! The ephemeral harness-hosted startup [`Seed`] — the api-bridging data the

+//! harness hands spt-core at `SessionStart`. A `SessionStart`-equivalent hook

+//! fires `api seed --pid <parent_pid> --session-id <sid>`; a later

+//! `api listen <id>` consumes it (matched by the listener's *own* parent pid),

-//! CONTEXT describes the M3 daemon holding this in-memory; M2a mirrors M1's

-//! standalone model with a file instead. **This module is the localized swap

-//! point** — M3 replaces the file read/write with the daemon's in-memory map

-//! and nothing else in the `api listen` path changes.

+//! **The transport is the daemon's in-memory map, not a file (M3b / B6).** The

+//! interim no-daemon model (M1/M2a) bridged the two `api` invocations through a

+//! seed *file* under `<spt_home>/seeds/`; M3b retires that file bridge

+//! (REQ-START-3). The seed now lives in the per-machine daemon's memory —

+//! `api seed` PUTs it and `api listen` TAKEs it over the seed-control IPC

+//! ([`spt_daemon::seedmap`]). This module keeps only the **shared data

+//! contract** ([`Seed`]); the put/take/consume mechanics live in the daemon.

-/// share directly, minus the not-yet-chosen `live_id`.

+/// share directly, minus the not-yet-chosen `live_id`. The harness fills it at

+/// `api seed`; the daemon holds it in memory keyed by [`Seed::parent_pid`] until

-pub fn seed_path(parent_pid: u32) -> PathBuf {

-    seeds_dir().join(format!("{parent_pid}.json"))

-/// Write (or refresh) the seed keyed by its `parent_pid`. Atomic (tmp+rename).

-pub fn write_seed(seed: &Seed) -> io::Result<()> {

-    std::fs::create_dir_all(seeds_dir())?;

-    let json = serde_json::to_string(seed)

-        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

-    atomic_write_string(&seed_path(seed.parent_pid), &json)

-/// Read the seed for `parent_pid`, if present and parseable.

-pub fn read_seed(parent_pid: u32) -> Option<Seed> {

-    let content = std::fs::read_to_string(seed_path(parent_pid)).ok()?;

-    serde_json::from_str(&content).ok()

-/// Remove a consumed seed (best-effort; seeds are ephemeral).

-    let _ = std::fs::remove_file(seed_path(parent_pid));

-    static ENV_LOCK: Mutex<()> = Mutex::new(());

-        _lock: std::sync::MutexGuard<'static, ()>,

-            let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());

-            let tmp = tempfile::tempdir().unwrap();

-            let prev = std::env::var("SPT_HOME").ok();

-            std::env::set_var("SPT_HOME", tmp.path());

-            Home { _lock: lock, _tmp: tmp, prev }

-                Some(v) => std::env::set_var("SPT_HOME", v),

-                None => std::env::remove_var("SPT_HOME"),

-    // [unit->REQ-START-1] a written seed reads back identically, keyed by pid.

+    // [unit->REQ-START-1] the seed data contract round-trips through serde — the

+    // shape the harness writes at `api seed` and the daemon hands back at

+    // `api listen` is stable across the JSON the seed-control IPC carries.

+    fn seed_round_trips_through_json() {

-        assert_eq!(read_seed(4321), Some(seed));

+        let json = serde_json::to_string(&seed).unwrap();

+        assert_eq!(serde_json::from_str::<Seed>(&json).unwrap(), seed);

-    // [unit->REQ-START-1] absent seed reads None; remove clears it.

+    // [unit->REQ-START-1] an absent cwd is omitted on the wire (skip_serializing)

+    // and decodes back to None — the optional working dir stays optional.

-    fn missing_seed_is_none_and_remove_clears() {

-        assert_eq!(read_seed(999), None);

-        assert!(read_seed(999).is_some());

+        assert!(!json.contains("cwd"), "None cwd is skipped: {json}");


Now the spt CLI: add hidden `daemon` subcommand.


    /// Harness-contract inbound surface (REQ-API-1): the entry points a
    /// harness's hooks fire to mutate on-disk SPT state.
    Api(crate::api::ApiArgs),
}

    /// Harness-contract inbound surface (REQ-API-1): the entry points a
    /// harness's hooks fire to mutate on-disk SPT state.
    Api(crate::api::ApiArgs),
    /// Run the per-machine broker/brain daemon (REQ-DAEMON-3). Internal: an
    /// `api` invocation auto-starts this; operators rarely run it directly.
    #[command(hide = true)]
    Daemon,
}


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

    /// Harness-contract inbound surface (REQ-API-1): the entry points a
    /// harness's hooks fire to mutate on-disk SPT state.
    Api(crate::api::ApiArgs),
}

    /// Harness-contract inbound surface (REQ-API-1): the entry points a
    /// harness's hooks fire to mutate on-disk SPT state.
    Api(crate::api::ApiArgs),
    /// Run the per-machine broker/brain daemon (REQ-DAEMON-3). Internal: an
    /// `api` invocation auto-starts this; operators rarely run it directly.
    #[command(hide = true)]
    Daemon,
}

+    /// Run the per-machine broker/brain daemon (REQ-DAEMON-3). Internal: an

+    /// `api` invocation auto-starts this; operators rarely run it directly.

/// Parse args and dispatch. Returns the process exit code.


        Cmd::Whoami => cmd_whoami(),
        Cmd::Api(args) => crate::api::run(args),
    }
}

        Cmd::Whoami => cmd_whoami(),
        Cmd::Api(args) => crate::api::run(args),
        Cmd::Daemon => cmd_daemon(),
    }
}

/// Run the per-machine daemon (the hidden `spt daemon` entry point). Blocks
/// serving the broker + seed-control channels until killed; a bind contention
/// (another daemon already owns this home) surfaces as a non-zero exit.
// [impl->REQ-DAEMON-3]
fn cmd_daemon() -> i32 {
    match spt_daemon::Daemon::run() {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("DAEMON_EXIT: {e}");
            1
        }
    }
}


        Cmd::Whoami => cmd_whoami(),
        Cmd::Api(args) => crate::api::run(args),
    }
}

        Cmd::Whoami => cmd_whoami(),
        Cmd::Api(args) => crate::api::run(args),
        Cmd::Daemon => cmd_daemon(),
    }
}

/// Run the per-machine daemon (the hidden `spt daemon` entry point). Blocks
/// serving the broker + seed-control channels until killed; a bind contention
/// (another daemon already owns this home) surfaces as a non-zero exit.
// [impl->REQ-DAEMON-3]
fn cmd_daemon() -> i32 {
    match spt_daemon::Daemon::run() {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("DAEMON_EXIT: {e}");
            1
        }
    }
}

+/// Run the per-machine daemon (the hidden `spt daemon` entry point). Blocks

+/// serving the broker + seed-control channels until killed; a bind contention

+/// (another daemon already owns this home) surfaces as a non-zero exit.

+            eprintln!("DAEMON_EXIT: {e}");

/// Read stdin to a string (message body source for send/ring).


        assert!(matches!(parse(&["spt", "whoami"]).unwrap().cmd, Cmd::Whoami));
    }

        assert!(matches!(parse(&["spt", "whoami"]).unwrap().cmd, Cmd::Whoami));
        // The hidden daemon entry point parses (auto-started by `api`).
        assert!(matches!(parse(&["spt", "daemon"]).unwrap().cmd, Cmd::Daemon));
    }


        assert!(matches!(parse(&["spt", "whoami"]).unwrap().cmd, Cmd::Whoami));
    }

        assert!(matches!(parse(&["spt", "whoami"]).unwrap().cmd, Cmd::Whoami));
        // The hidden daemon entry point parses (auto-started by `api`).
        assert!(matches!(parse(&["spt", "daemon"]).unwrap().cmd, Cmd::Daemon));
    }

assert!(matches!(parse(&["spt", "stop", "bob"]).unwrap().cmd, Cmd::Stop { .. }));

+        // The hidden daemon entry point parses (auto-started by `api`).

+        assert!(matches!(parse(&["spt", "daemon"]).unwrap().cmd, Cmd::Daemon));

// [unit->REQ-MSG-2] ring timeout defaults to 60s when unspecified.


Now api/mod.rs: ensure_daemon at top of run().


/// Parse + dispatch the `api` group. Returns the process exit code.
pub fn run(args: ApiArgs) -> i32 {
    // Resolve the manifest up front so a bad path/content fails fast and
    // uniformly, before any state mutation.

/// Auto-start the per-machine daemon if absent (REQ-DAEMON-3). Best-effort: a
/// start failure is surfaced but does not abort commands that don't need the
/// daemon (e.g. `capability`); the seed/listen path re-checks and fails loudly if
/// the daemon is genuinely unreachable.
// [impl->REQ-DAEMON-3]
fn ensure_daemon() {
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_WARN: {e}");
    }
}

/// Parse + dispatch the `api` group. Returns the process exit code.
pub fn run(args: ApiArgs) -> i32 {
    // Any api invocation brings the daemon up if absent — the reliable anchor
    // every harness hook shares (REQ-DAEMON-3).
    ensure_daemon();

    // Resolve the manifest up front so a bad path/content fails fast and
    // uniformly, before any state mutation.


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

/// Parse + dispatch the `api` group. Returns the process exit code.
pub fn run(args: ApiArgs) -> i32 {
    // Resolve the manifest up front so a bad path/content fails fast and
    // uniformly, before any state mutation.

/// Auto-start the per-machine daemon if absent (REQ-DAEMON-3). Best-effort: a
/// start failure is surfaced but does not abort commands that don't need the
/// daemon (e.g. `capability`); the seed/listen path re-checks and fails loudly if
/// the daemon is genuinely unreachable.
// [impl->REQ-DAEMON-3]
fn ensure_daemon() {
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_WARN: {e}");
    }
}

/// Parse + dispatch the `api` group. Returns the process exit code.
pub fn run(args: ApiArgs) -> i32 {
    // Any api invocation brings the daemon up if absent — the reliable anchor
    // every harness hook shares (REQ-DAEMON-3).
    ensure_daemon();

    // Resolve the manifest up front so a bad path/content fails fast and
    // uniformly, before any state mutation.

+/// Auto-start the per-machine daemon if absent (REQ-DAEMON-3). Best-effort: a

+/// start failure is surfaced but does not abort commands that don't need the

+/// daemon (e.g. `capability`); the seed/listen path re-checks and fails loudly if

+/// the daemon is genuinely unreachable.

+    if let Err(e) = spt_daemon::ensure_running() {

+        eprintln!("DAEMON_START_WARN: {e}");

+    // Any api invocation brings the daemon up if absent — the reliable anchor

+    // every harness hook shares (REQ-DAEMON-3).


Now rewire startup.rs: cmd_seed (put to daemon), bind_from_seed (take from daemon), imports, and tests.


use spt_msg::listener::PollListener;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::proc;
use spt_store::seed::{self, Seed};
use spt_store::spool;

use spt_msg::listener::PollListener;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::proc;
use spt_store::seed::Seed;
use spt_store::spool;


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

use spt_msg::listener::PollListener;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::proc;
use spt_store::seed::{self, Seed};
use spt_store::spool;

use spt_msg::listener::PollListener;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::proc;
use spt_store::seed::Seed;
use spt_store::spool;


//! Flow (CONTEXT §Startup flows, harness-hosted):
//! 1. The harness's SessionStart hook fires `api seed --pid <parent_pid>
//!    --session-id <sid>` → an ephemeral seed file keyed by the stable parent
//!    pid (REQ-START-1).
//! 2. The agent runs its `$SPT listen <id>` alias → `api listen` self-discovers
//!    its parent pid, matches the seed, validates it against pid-recycle
//!    (KNOWN-HAZARDS 5.1), binds the perch skeleton→live, and relays.
//! 3. Alternatively a just-spawned binary fires `api bind <id>` to associate
//!    itself post-spawn (REQ-SEAM-POSTSPAWN) — establishing the live perch on
//!    first contact, or flipping an existing skeleton.
//
// [impl->REQ-START-1]
// [impl->REQ-START-2]
// [impl->REQ-SEAM-POSTSPAWN]

//! Flow (CONTEXT §Startup flows, harness-hosted):
//! 1. The harness's SessionStart hook fires `api seed --pid <parent_pid>
//!    --session-id <sid>` → the seed is stored in the **daemon's in-memory map**
//!    keyed by the stable parent pid (REQ-START-3; no file — B6 retired the
//!    `spt_store::seed` file bridge).
//! 2. The agent runs its `$SPT listen <id>` alias → `api listen` self-discovers
//!    its parent pid, TAKEs the seed from the daemon, validates it against
//!    pid-recycle (KNOWN-HAZARDS 5.1), binds the perch skeleton→live, and relays.
//! 3. Alternatively a just-spawned binary fires `api bind <id>` to associate
//!    itself post-spawn (REQ-SEAM-POSTSPAWN) — establishing the live perch on
//!    first contact, or flipping an existing skeleton.
//
// [impl->REQ-START-2]
// [impl->REQ-START-3]
// [impl->REQ-SEAM-POSTSPAWN]


//! Flow (CONTEXT §Startup flows, harness-hosted):
//! 1. The harness's SessionStart hook fires `api seed --pid <parent_pid>
//!    --session-id <sid>` → an ephemeral seed file keyed by the stable parent
//!    pid (REQ-START-1).
//! 2. The agent runs its `$SPT listen <id>` alias → `api listen` self-discovers
//!    its parent pid, matches the seed, validates it against pid-recycle
//!    (KNOWN-HAZARDS 5.1), binds the perch skeleton→live, and relays.
//! 3. Alternatively a just-spawned binary fires `api bind <id>` to associate
//!    itself post-spawn (REQ-SEAM-POSTSPAWN) — establishing the live perch on
//!    first contact, or flipping an existing skeleton.
//
// [impl->REQ-START-1]
// [impl->REQ-START-2]
// [impl->REQ-SEAM-POSTSPAWN]

//! Flow (CONTEXT §Startup flows, harness-hosted):
//! 1. The harness's SessionStart hook fires `api seed --pid <parent_pid>
//!    --session-id <sid>` → the seed is stored in the **daemon's in-memory map**
//!    keyed by the stable parent pid (REQ-START-3; no file — B6 retired the
//!    `spt_store::seed` file bridge).
//! 2. The agent runs its `$SPT listen <id>` alias → `api listen` self-discovers
//!    its parent pid, TAKEs the seed from the daemon, validates it against
//!    pid-recycle (KNOWN-HAZARDS 5.1), binds the perch skeleton→live, and relays.
//! 3. Alternatively a just-spawned binary fires `api bind <id>` to associate
//!    itself post-spawn (REQ-SEAM-POSTSPAWN) — establishing the live perch on
//!    first contact, or flipping an existing skeleton.
//
// [impl->REQ-START-2]
// [impl->REQ-START-3]
// [impl->REQ-SEAM-POSTSPAWN]

-//!    --session-id <sid>` → an ephemeral seed file keyed by the stable parent

+//!    --session-id <sid>` → the seed is stored in the **daemon's in-memory map**

+//!    keyed by the stable parent pid (REQ-START-3; no file — B6 retired the

-//!    its parent pid, matches the seed, validates it against pid-recycle

-//!    (KNOWN-HAZARDS 5.1), binds the perch skeleton→live, and relays.

+//!    its parent pid, TAKEs the seed from the daemon, validates it against

+//!    pid-recycle (KNOWN-HAZARDS 5.1), binds the perch skeleton→live, and relays.


/// `api seed` — record the ephemeral startup seed keyed by parent pid. No prior
/// perch exists, so this call is not auth-gated (it *establishes* the binding
/// material the gated calls later authenticate against).
// [impl->REQ-START-1]
pub fn cmd_seed(ctx: &Ctx, parent_pid: u32, session_id: String) -> i32 {
    if session_id.is_empty() {
        eprintln!("USAGE: seed requires a non-empty --session-id");
        return EXIT_REFUSED;
    }
    let seed = Seed {
        parent_pid,
        session_id,
        adapter: ctx.adapter.clone(),
        cwd: std::env::current_dir()
            .ok()
            .map(|p| p.to_string_lossy().replace('\\', "/")),
        created: now_stamp(),
    };
    match seed::write_seed(&seed) {
        Ok(()) => {
            eprintln!("SEEDED:{parent_pid}");
            0
        }
        Err(e) => {
            eprintln!("SEED_FAIL:{parent_pid}: {e}");
            EXIT_REFUSED
        }
    }
}

/// `api seed` — record the ephemeral startup seed keyed by parent pid. No prior
/// perch exists, so this call is not auth-gated (it *establishes* the binding
/// material the gated calls later authenticate against).
///
/// REQ-START-3: the seed is PUT into the daemon's in-memory map over the
/// seed-control IPC — **no file is written**. The daemon is auto-started if
/// absent (REQ-DAEMON-3).
// [impl->REQ-START-3]
pub fn cmd_seed(ctx: &Ctx, parent_pid: u32, session_id: String) -> i32 {
    if session_id.is_empty() {
        eprintln!("USAGE: seed requires a non-empty --session-id");
        return EXIT_REFUSED;
    }
    let seed = Seed {
        parent_pid,
        session_id,
        adapter: ctx.adapter.clone(),
        cwd: std::env::current_dir()
            .ok()
            .map(|p| p.to_string_lossy().replace('\\', "/")),
        created: now_stamp(),
    };
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("SEED_FAIL:{parent_pid}: daemon unavailable: {e}");
        return EXIT_REFUSED;
    }
    match spt_daemon::put_seed(&spt_daemon::seed_socket_name(), &seed) {
        Ok(()) => {
            eprintln!("SEEDED:{parent_pid}");
            0
        }
        Err(e) => {
            eprintln!("SEED_FAIL:{parent_pid}: {e}");
            EXIT_REFUSED
        }
    }
}


/// `api seed` — record the ephemeral startup seed keyed by parent pid. No prior
/// perch exists, so this call is not auth-gated (it *establishes* the binding
/// material the gated calls later authenticate against).
// [impl->REQ-START-1]
pub fn cmd_seed(ctx: &Ctx, parent_pid: u32, session_id: String) -> i32 {
    if session_id.is_empty() {
        eprintln!("USAGE: seed requires a non-empty --session-id");
        return EXIT_REFUSED;
    }
    let seed = Seed {
        parent_pid,
        session_id,
        adapter: ctx.adapter.clone(),
        cwd: std::env::current_dir()
            .ok()
            .map(|p| p.to_string_lossy().replace('\\', "/")),
        created: now_stamp(),
    };
    match seed::write_seed(&seed) {
        Ok(()) => {
            eprintln!("SEEDED:{parent_pid}");
            0
        }
        Err(e) => {
            eprintln!("SEED_FAIL:{parent_pid}: {e}");
            EXIT_REFUSED
        }
    }
}

/// `api seed` — record the ephemeral startup seed keyed by parent pid. No prior
/// perch exists, so this call is not auth-gated (it *establishes* the binding
/// material the gated calls later authenticate against).
///
/// REQ-START-3: the seed is PUT into the daemon's in-memory map over the
/// seed-control IPC — **no file is written**. The daemon is auto-started if
/// absent (REQ-DAEMON-3).
// [impl->REQ-START-3]
pub fn cmd_seed(ctx: &Ctx, parent_pid: u32, session_id: String) -> i32 {
    if session_id.is_empty() {
        eprintln!("USAGE: seed requires a non-empty --session-id");
        return EXIT_REFUSED;
    }
    let seed = Seed {
        parent_pid,
        session_id,
        adapter: ctx.adapter.clone(),
        cwd: std::env::current_dir()
            .ok()
            .map(|p| p.to_string_lossy().replace('\\', "/")),
        created: now_stamp(),
    };
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("SEED_FAIL:{parent_pid}: daemon unavailable: {e}");
        return EXIT_REFUSED;
    }
    match spt_daemon::put_seed(&spt_daemon::seed_socket_name(), &seed) {
        Ok(()) => {
            eprintln!("SEEDED:{parent_pid}");
            0
        }
        Err(e) => {
            eprintln!("SEED_FAIL:{parent_pid}: {e}");
            EXIT_REFUSED
        }
    }
}

+/// REQ-START-3: the seed is PUT into the daemon's in-memory map over the

+/// seed-control IPC — **no file is written**. The daemon is auto-started if

+    if let Err(e) = spt_daemon::ensure_running() {

+        eprintln!("SEED_FAIL:{parent_pid}: daemon unavailable: {e}");

+    match spt_daemon::put_seed(&spt_daemon::seed_socket_name(), &seed) {


/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.
/// Pure of any listener I/O so it is unit-testable without spawning processes.
/// Returns the perch's capability token on success.
// [impl->REQ-START-2]
// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]
pub fn bind_from_seed(id: &str, parent_pid: u32) -> Result<String, BindError> {
    let seed = seed::read_seed(parent_pid).ok_or(BindError::NoSeed(parent_pid))?;
    if seed.session_id.is_empty() {
        return Err(BindError::EmptySession);
    }
    // Pid-recycle defense (5.1): anchor liveness on the stable parent pid. A
    // dead anchor means the harness that wrote the seed is gone — a recycled or
    // abandoned seed must never bind.
    if !proc::is_process_alive(seed.parent_pid) {
        return Err(BindError::StaleSeed(seed.parent_pid));
    }
    let token = establish_perch(id, &seed.session_id, seed.parent_pid)?;
    seed::remove_seed(parent_pid); // consumed; refreshed on the next SessionStart
    Ok(token)
}

/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.
/// Pure of any listener I/O so it is unit-testable without spawning processes.
/// Returns the perch's capability token on success.
///
/// REQ-START-3: the seed is TAKEn (consume-once) from the daemon's in-memory map
/// — the take itself consumes it, so a stale/conflicting seed is spent and the
/// harness re-seeds on the next SessionStart rather than leaving it to re-bind.
// [impl->REQ-START-2]
// [impl->REQ-START-3]
// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]
pub fn bind_from_seed(id: &str, parent_pid: u32) -> Result<String, BindError> {
    let seed = take_seed_from_daemon(parent_pid)?.ok_or(BindError::NoSeed(parent_pid))?;
    if seed.session_id.is_empty() {
        return Err(BindError::EmptySession);
    }
    // Pid-recycle defense (5.1): anchor liveness on the stable parent pid. A
    // dead anchor means the harness that wrote the seed is gone — a recycled or
    // abandoned seed must never bind.
    if !proc::is_process_alive(seed.parent_pid) {
        return Err(BindError::StaleSeed(seed.parent_pid));
    }
    establish_perch(id, &seed.session_id, seed.parent_pid)
}

/// TAKE (consume) the seed for `parent_pid` from the daemon's in-memory map,
/// auto-starting the daemon if absent (REQ-DAEMON-3 / REQ-START-3).
fn take_seed_from_daemon(parent_pid: u32) -> Result<Option<Seed>, BindError> {
    spt_daemon::ensure_running()
        .map_err(|e| BindError::Io(format!("daemon unavailable: {e}")))?;
    spt_daemon::take_seed(&spt_daemon::seed_socket_name(), parent_pid)
        .map_err(|e| BindError::Io(e.to_string()))
}


/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.
/// Pure of any listener I/O so it is unit-testable without spawning processes.
/// Returns the perch's capability token on success.
// [impl->REQ-START-2]
// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]
pub fn bind_from_seed(id: &str, parent_pid: u32) -> Result<String, BindError> {
    let seed = seed::read_seed(parent_pid).ok_or(BindError::NoSeed(parent_pid))?;
    if seed.session_id.is_empty() {
        return Err(BindError::EmptySession);
    }
    // Pid-recycle defense (5.1): anchor liveness on the stable parent pid. A
    // dead anchor means the harness that wrote the seed is gone — a recycled or
    // abandoned seed must never bind.
    if !proc::is_process_alive(seed.parent_pid) {
        return Err(BindError::StaleSeed(seed.parent_pid));
    }
    let token = establish_perch(id, &seed.session_id, seed.parent_pid)?;
    seed::remove_seed(parent_pid); // consumed; refreshed on the next SessionStart
    Ok(token)
}

/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.
/// Pure of any listener I/O so it is unit-testable without spawning processes.
/// Returns the perch's capability token on success.
///
/// REQ-START-3: the seed is TAKEn (consume-once) from the daemon's in-memory map
/// — the take itself consumes it, so a stale/conflicting seed is spent and the
/// harness re-seeds on the next SessionStart rather than leaving it to re-bind.
// [impl->REQ-START-2]
// [impl->REQ-START-3]
// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]
pub fn bind_from_seed(id: &str, parent_pid: u32) -> Result<String, BindError> {
    let seed = take_seed_from_daemon(parent_pid)?.ok_or(BindError::NoSeed(parent_pid))?;
    if seed.session_id.is_empty() {
        return Err(BindError::EmptySession);
    }
    // Pid-recycle defense (5.1): anchor liveness on the stable parent pid. A
    // dead anchor means the harness that wrote the seed is gone — a recycled or
    // abandoned seed must never bind.
    if !proc::is_process_alive(seed.parent_pid) {
        return Err(BindError::StaleSeed(seed.parent_pid));
    }
    establish_perch(id, &seed.session_id, seed.parent_pid)
}

/// TAKE (consume) the seed for `parent_pid` from the daemon's in-memory map,
/// auto-starting the daemon if absent (REQ-DAEMON-3 / REQ-START-3).
fn take_seed_from_daemon(parent_pid: u32) -> Result<Option<Seed>, BindError> {
    spt_daemon::ensure_running()
        .map_err(|e| BindError::Io(format!("daemon unavailable: {e}")))?;
    spt_daemon::take_seed(&spt_daemon::seed_socket_name(), parent_pid)
        .map_err(|e| BindError::Io(e.to_string()))
}

+/// REQ-START-3: the seed is TAKEn (consume-once) from the daemon's in-memory map

+/// — the take itself consumes it, so a stale/conflicting seed is spent and the

+/// harness re-seeds on the next SessionStart rather than leaving it to re-bind.

-    let seed = seed::read_seed(parent_pid).ok_or(BindError::NoSeed(parent_pid))?;

+    let seed = take_seed_from_daemon(parent_pid)?.ok_or(BindError::NoSeed(parent_pid))?;

-    let token = establish_perch(id, &seed.session_id, seed.parent_pid)?;

-    seed::remove_seed(parent_pid); // consumed; refreshed on the next SessionStart

+    establish_perch(id, &seed.session_id, seed.parent_pid)

+/// TAKE (consume) the seed for `parent_pid` from the daemon's in-memory map,

+/// auto-starting the daemon if absent (REQ-DAEMON-3 / REQ-START-3).

+fn take_seed_from_daemon(parent_pid: u32) -> Result<Option<Seed>, BindError> {

+        .map_err(|e| BindError::Io(format!("daemon unavailable: {e}")))?;

+    spt_daemon::take_seed(&spt_daemon::seed_socket_name(), parent_pid)

+        .map_err(|e| BindError::Io(e.to_string()))

/// Write the perch record (skeleton→live) + ready marker + capability token.

/// Reviving an existing perch keeps its token; a live perch held under a

/// different session id is a conflict (pid-recycle / id collision guard).


Now adapt startup.rs tests to the daemon-backed seed. Rewrite the test helper + each seed test.


#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::isolated_home;

    fn write_seed(parent_pid: u32, session_id: &str) {
        seed::write_seed(&Seed {
            parent_pid,
            session_id: session_id.into(),
            adapter: "mock".into(),
            cwd: None,
            created: "0".into(),
        })
        .unwrap();
    }

    #[test] // [unit->REQ-START-2] a valid seed (live anchor) binds a live perch.
    fn valid_seed_binds_live_perch() {
        let _h = isolated_home();
        // Use this very process as the (alive) anchor.
        let anchor = std::process::id();
        write_seed(anchor, "sid-1");
        let token = bind_from_seed("alice", anchor).expect("bind");
        assert_eq!(token.len(), 32);

        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
        let rec = info::read_info(&perch_path).expect("info.json");
        assert_eq!(rec.session_id, "sid-1");
        assert_eq!(rec.state, "live_agent");
        assert_eq!(rec.parent_pid, Some(anchor));
        assert!(perch::resolve_ready_file("alice", ParentHint::Infer).exists());
        // Seed is consumed.
        assert!(seed::read_seed(anchor).is_none());
    }

    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a dead-anchor seed never binds.
    fn stale_seed_with_dead_anchor_refused() {
        let _h = isolated_home();
        const DEAD: u32 = 2_000_000_000;
        write_seed(DEAD, "sid-1");
        let err = bind_from_seed("alice", DEAD).unwrap_err();
        assert_eq!(err, BindError::StaleSeed(DEAD));
        // No perch was created.
        assert!(info::read_info(&perch::resolve_perch_path("alice", ParentHint::Infer)).is_none());
    }

    #[test] // [unit->REQ-START-2] absent seed is refused, not bound.
    fn missing_seed_refused() {
        let _h = isolated_home();
        let err = bind_from_seed("alice", std::process::id()).unwrap_err();
        assert!(matches!(err, BindError::NoSeed(_)));
    }

    #[test] // [unit->REQ-START-2] empty session id in the seed is refused.
    fn empty_session_seed_refused() {
        let _h = isolated_home();
        let anchor = std::process::id();
        write_seed(anchor, "");
        assert_eq!(bind_from_seed("alice", anchor).unwrap_err(), BindError::EmptySession);
    }

    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a live perch under another
    // session is not silently rebound (id-collision guard).
    fn live_perch_under_other_session_conflicts() {
        let _h = isolated_home();
        let anchor = std::process::id();
        // First bind establishes alice@sid-1 owned by this (live) process.
        write_seed(anchor, "sid-1");
        bind_from_seed("alice", anchor).unwrap();
        // A second seed tries to claim alice for a different session.
        write_seed(anchor, "sid-2");
        let err = bind_from_seed("alice", anchor).unwrap_err();
        assert_eq!(err, BindError::Conflict { id: "alice".into(), held: "sid-1".into() });
    }

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::isolated_home;
    use spt_daemon::SeedRegistry;
    use std::sync::Arc;
    use std::time::Duration;

    /// Stand up an in-process seed daemon on the current isolated home's seed
    /// socket and wait until it answers. Returns the shared registry so the test
    /// can seed it directly; the take path then consumes from it over the real
    /// IPC (no real `spt daemon` process spawned, since the channel already
    /// answers `ensure_running`'s ping). The serve thread is leaked (dies with the
    /// test process; each isolated home gets a distinct socket name).
    fn start_seed_daemon() -> Arc<SeedRegistry> {
        let reg = Arc::new(SeedRegistry::new());
        let name = spt_daemon::seed_socket_name();
        let srv = Arc::clone(&reg);
        std::thread::spawn(move || {
            let _ = spt_daemon::seedmap::serve_seed_control(&name, &srv);
        });
        for _ in 0..400 {
            if spt_daemon::is_running() {
                return reg;
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        panic!("seed daemon did not come up");
    }

    fn put_seed(reg: &SeedRegistry, parent_pid: u32, session_id: &str) {
        reg.put(Seed {
            parent_pid,
            session_id: session_id.into(),
            adapter: "mock".into(),
            cwd: None,
            created: "0".into(),
        });
    }

    #[test] // [unit->REQ-START-3] a valid seed (live anchor) binds a live perch.
    fn valid_seed_binds_live_perch() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        // Use this very process as the (alive) anchor.
        let anchor = std::process::id();
        put_seed(&reg, anchor, "sid-1");
        let token = bind_from_seed("alice", anchor).expect("bind");
        assert_eq!(token.len(), 32);

        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
        let rec = info::read_info(&perch_path).expect("info.json");
        assert_eq!(rec.session_id, "sid-1");
        assert_eq!(rec.state, "live_agent");
        assert_eq!(rec.parent_pid, Some(anchor));
        assert!(perch::resolve_ready_file("alice", ParentHint::Infer).exists());
        // Seed is consumed from the daemon's in-memory map (take-once).
        assert!(reg.peek(anchor).is_none());
    }

    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a dead-anchor seed never binds.
    fn stale_seed_with_dead_anchor_refused() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        const DEAD: u32 = 2_000_000_000;
        put_seed(&reg, DEAD, "sid-1");
        let err = bind_from_seed("alice", DEAD).unwrap_err();
        assert_eq!(err, BindError::StaleSeed(DEAD));
        // No perch was created.
        assert!(info::read_info(&perch::resolve_perch_path("alice", ParentHint::Infer)).is_none());
    }

    #[test] // [unit->REQ-START-3] absent seed is refused, not bound.
    fn missing_seed_refused() {
        let _h = isolated_home();
        let _reg = start_seed_daemon();
        let err = bind_from_seed("alice", std::process::id()).unwrap_err();
        assert!(matches!(err, BindError::NoSeed(_)));
    }

    #[test] // [unit->REQ-START-3] empty session id in the seed is refused.
    fn empty_session_seed_refused() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        let anchor = std::process::id();
        put_seed(&reg, anchor, "");
        assert_eq!(bind_from_seed("alice", anchor).unwrap_err(), BindError::EmptySession);
    }

    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a live perch under another
    // session is not silently rebound (id-collision guard).
    fn live_perch_under_other_session_conflicts() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        let anchor = std::process::id();
        // First bind establishes alice@sid-1 owned by this (live) process.
        put_seed(&reg, anchor, "sid-1");
        bind_from_seed("alice", anchor).unwrap();
        // A second seed tries to claim alice for a different session.
        put_seed(&reg, anchor, "sid-2");
        let err = bind_from_seed("alice", anchor).unwrap_err();
        assert_eq!(err, BindError::Conflict { id: "alice".into(), held: "sid-1".into() });
    }


#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::isolated_home;

    fn write_seed(parent_pid: u32, session_id: &str) {
        seed::write_seed(&Seed {
            parent_pid,
            session_id: session_id.into(),
            adapter: "mock".into(),
            cwd: None,
            created: "0".into(),
        })
        .unwrap();
    }

    #[test] // [unit->REQ-START-2] a valid seed (live anchor) binds a live perch.
    fn valid_seed_binds_live_perch() {
        let _h = isolated_home();
        // Use this very process as the (alive) anchor.
        let anchor = std::process::id();
        write_seed(anchor, "sid-1");
        let token = bind_from_seed("alice", anchor).expect("bind");
        assert_eq!(token.len(), 32);

        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
        let rec = info::read_info(&perch_path).expect("info.json");
        assert_eq!(rec.session_id, "sid-1");
        assert_eq!(rec.state, "live_agent");
        assert_eq!(rec.parent_pid, Some(anchor));
        assert!(perch::resolve_ready_file("alice", ParentHint::Infer).exists());
        // Seed is consumed.
        assert!(seed::read_seed(anchor).is_none());
    }

    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a dead-anchor seed never binds.
    fn stale_seed_with_dead_anchor_refused() {
        let _h = isolated_home();
        const DEAD: u32 = 2_000_000_000;
        write_seed(DEAD, "sid-1");
        let err = bind_from_seed("alice", DEAD).unwrap_err();
        assert_eq!(err, BindError::StaleSeed(DEAD));
        // No perch was created.
        assert!(info::read_info(&perch::resolve_perch_path("alice", ParentHint::Infer)).is_none());
    }

    #[test] // [unit->REQ-START-2] absent seed is refused, not bound.
    fn missing_seed_refused() {
        let _h = isolated_home();
        let err = bind_from_seed("alice", std::process::id()).unwrap_err();
        assert!(matches!(err, BindError::NoSeed(_)));
    }

    #[test] // [unit->REQ-START-2] empty session id in the seed is refused.
    fn empty_session_seed_refused() {
        let _h = isolated_home();
        let anchor = std::process::id();
        write_seed(anchor, "");
        assert_eq!(bind_from_seed("alice", anchor).unwrap_err(), BindError::EmptySession);
    }

    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a live perch under another
    // session is not silently rebound (id-collision guard).
    fn live_perch_under_other_session_conflicts() {
        let _h = isolated_home();
        let anchor = std::process::id();
        // First bind establishes alice@sid-1 owned by this (live) process.
        write_seed(anchor, "sid-1");
        bind_from_seed("alice", anchor).unwrap();
        // A second seed tries to claim alice for a different session.
        write_seed(anchor, "sid-2");
        let err = bind_from_seed("alice", anchor).unwrap_err();
        assert_eq!(err, BindError::Conflict { id: "alice".into(), held: "sid-1".into() });
    }

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::isolated_home;
    use spt_daemon::SeedRegistry;
    use std::sync::Arc;
    use std::time::Duration;

    /// Stand up an in-process seed daemon on the current isolated home's seed
    /// socket and wait until it answers. Returns the shared registry so the test
    /// can seed it directly; the take path then consumes from it over the real
    /// IPC (no real `spt daemon` process spawned, since the channel already
    /// answers `ensure_running`'s ping). The serve thread is leaked (dies with the
    /// test process; each isolated home gets a distinct socket name).
    fn start_seed_daemon() -> Arc<SeedRegistry> {
        let reg = Arc::new(SeedRegistry::new());
        let name = spt_daemon::seed_socket_name();
        let srv = Arc::clone(&reg);
        std::thread::spawn(move || {
            let _ = spt_daemon::seedmap::serve_seed_control(&name, &srv);
        });
        for _ in 0..400 {
            if spt_daemon::is_running() {
                return reg;
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        panic!("seed daemon did not come up");
    }

    fn put_seed(reg: &SeedRegistry, parent_pid: u32, session_id: &str) {
        reg.put(Seed {
            parent_pid,
            session_id: session_id.into(),
            adapter: "mock".into(),
            cwd: None,
            created: "0".into(),
        });
    }

    #[test] // [unit->REQ-START-3] a valid seed (live anchor) binds a live perch.
    fn valid_seed_binds_live_perch() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        // Use this very process as the (alive) anchor.
        let anchor = std::process::id();
        put_seed(&reg, anchor, "sid-1");
        let token = bind_from_seed("alice", anchor).expect("bind");
        assert_eq!(token.len(), 32);

        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
        let rec = info::read_info(&perch_path).expect("info.json");
        assert_eq!(rec.session_id, "sid-1");
        assert_eq!(rec.state, "live_agent");
        assert_eq!(rec.parent_pid, Some(anchor));
        assert!(perch::resolve_ready_file("alice", ParentHint::Infer).exists());
        // Seed is consumed from the daemon's in-memory map (take-once).
        assert!(reg.peek(anchor).is_none());
    }

    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a dead-anchor seed never binds.
    fn stale_seed_with_dead_anchor_refused() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        const DEAD: u32 = 2_000_000_000;
        put_seed(&reg, DEAD, "sid-1");
        let err = bind_from_seed("alice", DEAD).unwrap_err();
        assert_eq!(err, BindError::StaleSeed(DEAD));
        // No perch was created.
        assert!(info::read_info(&perch::resolve_perch_path("alice", ParentHint::Infer)).is_none());
    }

    #[test] // [unit->REQ-START-3] absent seed is refused, not bound.
    fn missing_seed_refused() {
        let _h = isolated_home();
        let _reg = start_seed_daemon();
        let err = bind_from_seed("alice", std::process::id()).unwrap_err();
        assert!(matches!(err, BindError::NoSeed(_)));
    }

    #[test] // [unit->REQ-START-3] empty session id in the seed is refused.
    fn empty_session_seed_refused() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        let anchor = std::process::id();
        put_seed(&reg, anchor, "");
        assert_eq!(bind_from_seed("alice", anchor).unwrap_err(), BindError::EmptySession);
    }

    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a live perch under another
    // session is not silently rebound (id-collision guard).
    fn live_perch_under_other_session_conflicts() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        let anchor = std::process::id();
        // First bind establishes alice@sid-1 owned by this (live) process.
        put_seed(&reg, anchor, "sid-1");
        bind_from_seed("alice", anchor).unwrap();
        // A second seed tries to claim alice for a different session.
        put_seed(&reg, anchor, "sid-2");
        let err = bind_from_seed("alice", anchor).unwrap_err();
        assert_eq!(err, BindError::Conflict { id: "alice".into(), held: "sid-1".into() });
    }

-    fn write_seed(parent_pid: u32, session_id: &str) {

+    /// Stand up an in-process seed daemon on the current isolated home's seed

+    /// socket and wait until it answers. Returns the shared registry so the test

+    /// can seed it directly; the take path then consumes from it over the real

+    /// IPC (no real `spt daemon` process spawned, since the channel already

+    /// answers `ensure_running`'s ping). The serve thread is leaked (dies with the

+    /// test process; each isolated home gets a distinct socket name).

+    fn start_seed_daemon() -> Arc<SeedRegistry> {

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

+        let name = spt_daemon::seed_socket_name();

+            let _ = spt_daemon::seedmap::serve_seed_control(&name, &srv);

+            if spt_daemon::is_running() {

+            std::thread::sleep(Duration::from_millis(5));

+        panic!("seed daemon did not come up");

+    fn put_seed(reg: &SeedRegistry, parent_pid: u32, session_id: &str) {

-    #[test] // [unit->REQ-START-2] a valid seed (live anchor) binds a live perch.

+    #[test] // [unit->REQ-START-3] a valid seed (live anchor) binds a live perch.

+        put_seed(&reg, anchor, "sid-1");

-        assert!(seed::read_seed(anchor).is_none());

+        // Seed is consumed from the daemon's in-memory map (take-once).

+        assert!(reg.peek(anchor).is_none());

-    #[test] // [unit->REQ-START-2] absent seed is refused, not bound.

+    #[test] // [unit->REQ-START-3] absent seed is refused, not bound.

+        let _reg = start_seed_daemon();

-    #[test] // [unit->REQ-START-2] empty session id in the seed is refused.

+    #[test] // [unit->REQ-START-3] empty session id in the seed is refused.

+        put_seed(&reg, anchor, "sid-2");


Build the whole workspace to catch wiring errors.


   Compiling spt-store v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling spt-msg v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-term v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-live v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling spt-daemon v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.23s


Now adapt the integration tests. Add helpers + pre-start an in-process daemon in the three existing tests (keep them hermetic), then add a real cold-start test.


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

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

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

/// Best-effort cross-platform kill of a pid (cleans up a real spawned daemon).
fn kill_pid(pid: u32) {
    #[cfg(windows)]
    let _ = Command::new("taskkill").args(["/PID", &pid.to_string(), "/F", "/T"]).output();
    #[cfg(unix)]
    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
}


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

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

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

/// Best-effort cross-platform kill of a pid (cleans up a real spawned daemon).
fn kill_pid(pid: u32) {
    #[cfg(windows)]
    let _ = Command::new("taskkill").args(["/PID", &pid.to_string(), "/F", "/T"]).output();
    #[cfg(unix)]
    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
}

+/// Stand up an **in-process** seed daemon on the current `SPT_HOME`'s seed

+/// socket so the child `spt api …` invocations find a running daemon (their

+/// `ensure_running` ping succeeds → no real `spt daemon` spawned). The serve

+/// thread is leaked; it dies with the test process. Call after `SPT_HOME` is set.

+    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());

+    let name = spt_daemon::seed_socket_name();

+        let _ = spt_daemon::seedmap::serve_seed_control(&name, &reg);

+        std::thread::sleep(Duration::from_millis(5));

+    panic!("in-process seed daemon did not come up");

+/// Best-effort cross-platform kill of a pid (cleans up a real spawned daemon).

+    let _ = Command::new("taskkill").args(["/PID", &pid.to_string(), "/F", "/T"]).output();

+    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();

let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());


Now add `start_inproc_daemon()` after each test sets SPT_HOME:


    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at

    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at


    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at

    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at

// (inherited via env). This test owns the process, so a plain set_var is safe.


    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();

    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();


    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();

    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();


    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // An absolute drop dir for the watched commune/signoff files (the committed

    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // An absolute drop dir for the watched commune/signoff files (the committed


    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // An absolute drop dir for the watched commune/signoff files (the committed

    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // An absolute drop dir for the watched commune/signoff files (the committed


Now add the cold-start real-daemon test at the end of the file:


    // Soft teardown removed the online marker (6.2).
    assert!(
        !perch::resolve_ready_file(id, ParentHint::Infer).exists(),
        "shutdown must soft-stop (remove the ready marker)"
    );
}

    // Soft teardown removed the online marker (6.2).
    assert!(
        !perch::resolve_ready_file(id, ParentHint::Infer).exists(),
        "shutdown must soft-stop (remove the ready marker)"
    );
}

/// Cold-start auto-start (REQ-DAEMON-3) + the in-memory seed handoff (REQ-START-3)
/// through a **real** spawned daemon: with no daemon pre-running, a cold
/// `spt api seed` transparently spins up `spt daemon`, the seed lands in its
/// in-memory map (no file), and a follow-up `api listen` TAKEs it and binds —
/// proving the cross-invocation handoff rides the daemon, not a seed file.
#[test]
fn cold_api_call_autostarts_daemon_and_handoff_is_in_memory() {
    // [int->REQ-DAEMON-3]
    // [int->REQ-START-3]
    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    // Deliberately do NOT pre-start a daemon — the cold `api seed` must spawn one.

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let id = "cold-e2e";
    let session_id = "sid-cold-1";
    let anchor = std::process::id().to_string();

    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered after cold start").unwrap();

    // Cold seed — auto-starts the daemon and PUTs the seed into its memory.
    let seed = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api seed");
    assert!(
        seed.status.success(),
        "cold api seed must auto-start the daemon and seed: {:?}\n{}",
        seed.status,
        String::from_utf8_lossy(&seed.stderr)
    );

    // The daemon is now up (it wrote its pid breadcrumb) — and no seed *file* was
    // written (the file bridge is dead: there is no seeds/ dir under the home).
    assert!(
        !home.path().join("seeds").exists(),
        "no seed file may be written — the seed lives in the daemon's memory"
    );

    // Listen TAKEs the seed from the daemon, binds, drains the backlog, exits.
    let listen = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api listen");
    let listen_ok = listen.status.success();
    let stdout = String::from_utf8_lossy(&listen.stdout).into_owned();
    let rec = spt_store::info::read_info(&perch_path);

    // Clean up the real spawned daemon before asserting (so a failure can't leak it).
    if let Ok(pid_txt) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(pid) = pid_txt.trim().parse::<u32>() {
            kill_pid(pid);
        }
    }

    assert!(listen_ok, "api listen exit: {:?}", listen.status);
    assert!(
        stdout.contains("delivered after cold start"),
        "listen must relay the spooled message via the in-memory seed handoff, got: {stdout:?}"
    );
    let rec = rec.expect("perch info.json after listen");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");
}


    // Soft teardown removed the online marker (6.2).
    assert!(
        !perch::resolve_ready_file(id, ParentHint::Infer).exists(),
        "shutdown must soft-stop (remove the ready marker)"
    );
}

/// Cold-start auto-start (REQ-DAEMON-3) + the in-memory seed handoff (REQ-START-3)
/// through a **real** spawned daemon: with no daemon pre-running, a cold
/// `spt api seed` transparently spins up `spt daemon`, the seed lands in its
/// in-memory map (no file), and a follow-up `api listen` TAKEs it and binds —
/// proving the cross-invocation handoff rides the daemon, not a seed file.
#[test]
fn cold_api_call_autostarts_daemon_and_handoff_is_in_memory() {
    // [int->REQ-DAEMON-3]
    // [int->REQ-START-3]
    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    // Deliberately do NOT pre-start a daemon — the cold `api seed` must spawn one.

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let id = "cold-e2e";
    let session_id = "sid-cold-1";
    let anchor = std::process::id().to_string();

    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered after cold start").unwrap();

    // Cold seed — auto-starts the daemon and PUTs the seed into its memory.
    let seed = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api seed");
    assert!(
        seed.status.success(),
        "cold api seed must auto-start the daemon and seed: {:?}\n{}",
        seed.status,
        String::from_utf8_lossy(&seed.stderr)
    );

    // The daemon is now up (it wrote its pid breadcrumb) — and no seed *file* was
    // written (the file bridge is dead: there is no seeds/ dir under the home).
    assert!(
        !home.path().join("seeds").exists(),
        "no seed file may be written — the seed lives in the daemon's memory"
    );

    // Listen TAKEs the seed from the daemon, binds, drains the backlog, exits.
    let listen = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api listen");
    let listen_ok = listen.status.success();
    let stdout = String::from_utf8_lossy(&listen.stdout).into_owned();
    let rec = spt_store::info::read_info(&perch_path);

    // Clean up the real spawned daemon before asserting (so a failure can't leak it).
    if let Ok(pid_txt) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(pid) = pid_txt.trim().parse::<u32>() {
            kill_pid(pid);
        }
    }

    assert!(listen_ok, "api listen exit: {:?}", listen.status);
    assert!(
        stdout.contains("delivered after cold start"),
        "listen must relay the spooled message via the in-memory seed handoff, got: {stdout:?}"
    );
    let rec = rec.expect("perch info.json after listen");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");
}

+/// Cold-start auto-start (REQ-DAEMON-3) + the in-memory seed handoff (REQ-START-3)

+/// through a **real** spawned daemon: with no daemon pre-running, a cold

+/// `spt api seed` transparently spins up `spt daemon`, the seed lands in its

+/// in-memory map (no file), and a follow-up `api listen` TAKEs it and binds —

+/// proving the cross-invocation handoff rides the daemon, not a seed file.

+    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());

+    let home = tempfile::tempdir().unwrap();

+    std::env::set_var("SPT_HOME", home.path());

+    // Deliberately do NOT pre-start a daemon — the cold `api seed` must spawn one.

+    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));

+    let anchor = std::process::id().to_string();

+    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

+    spool::spool_message_at(&perch_path, "tester", "delivered after cold start").unwrap();

+    // Cold seed — auto-starts the daemon and PUTs the seed into its memory.

+        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])

+        "cold api seed must auto-start the daemon and seed: {:?}\n{}",

+        String::from_utf8_lossy(&seed.stderr)

+    // The daemon is now up (it wrote its pid breadcrumb) — and no seed *file* was

+    // written (the file bridge is dead: there is no seeds/ dir under the home).

+        !home.path().join("seeds").exists(),

+        "no seed file may be written — the seed lives in the daemon's memory"

+    // Listen TAKEs the seed from the daemon, binds, drains the backlog, exits.

+    let listen = Command::new(&spt_bin)

+        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])

+    let listen_ok = listen.status.success();

+    let stdout = String::from_utf8_lossy(&listen.stdout).into_owned();

+    let rec = spt_store::info::read_info(&perch_path);

+    // Clean up the real spawned daemon before asserting (so a failure can't leak it).

+    if let Ok(pid_txt) = std::fs::read_to_string(home.path().join("daemon.pid")) {

+        if let Ok(pid) = pid_txt.trim().parse::<u32>() {

+    assert!(listen_ok, "api listen exit: {:?}", listen.status);

+        stdout.contains("delivered after cold start"),

+        "listen must relay the spooled message via the in-memory seed handoff, got: {stdout:?}"

+    let rec = rec.expect("perch info.json after listen");

+    assert_eq!(rec.session_id, session_id);

+    assert_eq!(rec.state, "live_agent");


Run unit tests (daemon, store-seed, spt lib).


Bash

cargo test -p spt-daemon -p spt-store --lib 2>&1 | tail -25


test seed::tests::seed_round_trips_through_json ... ok
test atomic::tests::atomic_write_creates_and_replaces ... ok
test liveness::tests::daemon_hosted_offline_overrides_live_pid ... ok
test liveness::tests::daemon_hosted_online_survives_dead_pid ... ok
test info::tests::corrupt_content_returns_none ... ok
test liveness::tests::external_perch_still_pid_probes ... ok
test info::tests::set_status_marks_existing_record ... ok
test liveness::tests::registry_entry_keys_on_status_for_hosted_rows ... ok
test registry::tests::resolve_on_empty_registry_is_none_not_error ... ok
test spool::tests::spool_touches_has_messages_sentinel ... ok
test registry::tests::register_and_lookup ... ok
test registry::tests::reregister_replaces_address ... ok
test spool::tests::reopen_is_idempotent ... ok
test registry::tests::resolve_cleans_before_lookup ... ok
test registry::tests::clean_removes_dead_keeps_live ... ok
test registry::tests::clean_keys_on_status_for_daemon_hosted_rows ... ok
test spool::tests::drain_marks_delivered_and_is_idempotent ... ok
test spool::tests::non_deferred_drain_skips_deferred_and_preserves_order ... ok
test spool::tests::deferred_row_survives_event_drain_then_hook_delivers ... ok
test spool::tests::event_part_body_persists_byte_exact ... ok
test registry::tests::concurrent_registration_never_locks ... ok
test info::tests::concurrent_read_during_writes_never_torn ... ok

test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.42s


cargo test -p spt-daemon 2>&1 | grep -E 'seedmap|daemon::|is_running|put_then|take_absent|put_refresh|test result'


test daemon::tests::is_running_tracks_the_seed_channel ... ok
test seedmap::tests::put_refreshes_existing ... ok
test seedmap::tests::put_then_take_consumes_once ... ok
test seedmap::tests::take_absent_is_none ... ok
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.29s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.19s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


Now spt lib (startup tests) + the integration test:


cargo test -p spt --lib 2>&1 | grep -E 'startup::|seed|bind|test result' | head -40


(Bash completed with no output)


error: no library targets found in package `spt`


cargo test -p spt --bin spt 2>&1 | grep -E 'startup::|test result' | head -30


test api::startup::tests::empty_session_seed_refused ... ok
test api::startup::tests::live_perch_under_other_session_conflicts ... ok
test api::startup::tests::missing_seed_refused ... ok
test api::startup::tests::post_spawn_bind_establishes_perch ... ok
test api::startup::tests::rebind_same_session_ok ... ok
test api::startup::tests::stale_seed_with_dead_anchor_refused ... ok
test api::startup::tests::valid_seed_binds_live_perch ... ok
test result: ok. 40 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.29s


Unit tests green. Now the integration test (real cold-start spawns a daemon):


cargo test -p spt --test contract_e2e 2>&1 | tail -30


Command running in background with ID: b9c2zb0wv. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\b9c2zb0wv.output. You will be notified when it completes. To check interim output, use Read on that file path.


<tool_use_error>Blocked: sleep 30 followed by: tail -30 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\b9c2zb0wv.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 30 followed by: tail -30 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\b9c2zb0wv.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.


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


Compiling still. Let me wait for the background task to finish.