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

    /// Register the perch (info.json + ready marker + listener address) and
    /// drain the offline backlog. Returns the agent and any backlog messages
    /// (oldest first) that were spooled while it was down.
    // [impl->REQ-MSG-3]
    pub fn start(id: &str) -> Result<(ReadyAgent, Vec<String>), String> {
        Self::start_homed(id, None)
    }

    /// [`start`](Self::start) with an explicit home-subnet pick for a NEW
    /// endpoint (D9-5, REQ-INST-15): home is assigned at creation through the
    /// shared seam ([`spt_store::home::stamp_creation_fields`]) — auto on a
    /// sole-subnet node, `subnet` required on a multi-subnet one
    /// (refuse-and-qualify), local-only when unpaired. A restart carries the
    /// durable creation-time fields forward (home is immutable; the resource
    /// blurb and knobs are user state a re-register must not wipe).
    // [impl->REQ-INST-15]
    pub fn start_homed(
        id: &str,
        subnet: Option<&str>,
    ) -> Result<(ReadyAgent, Vec<String>), String> {
        // [impl->REQ-HAZARD-ID-CHARSET] guard the id charset at the creation seam.
        spt_proto::id::validate_endpoint_id(id).map_err(|e| e.to_string())?;
        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
        fs::create_dir_all(&perch_path)
            .map_err(|e| format!("Failed to create perch dir: {}", e))?;

        // info.json first, so the record exists by the time the address is live.
        let prior = info::read_info(&perch_path);
        let mut rec =
            InfoJson::new(id, &now_stamp(), std::process::id(), &session_id(), "ready_agent");
        let subnets = spt_store::subnet::SubnetStore::load();
        let mut vis = spt_store::visibility::VisibilityStore::load();
        let dirty =
            spt_store::home::stamp_creation_fields(&mut rec, prior.as_ref(), &subnets, &mut vis, subnet, None)
                .map_err(|e| format!("HOME: {e}"))?;
        if dirty {
            let _ = vis.save();
        }
        info::write_info(&perch_path, &rec)
            .map_err(|e| format!("Failed to write info.json: {}", e))?;

//! Ready-agent lifecycle: register a perch, listen, drain backlog, tear down.
//!
//! Clean-room re-expression of the sister `owl/poll.rs` startup + `setup.rs`
//! against the spt-store API, scoped to the M1 loopback model. The harness
//! coupling that bloats the sister `poll.rs` (stream EVENT emission, EVENT-PART
//! chunking, alarms, handoff, Psyche) is **not** M1 — it lands in M2/M3. M1's
//! ready agent is the minimal participant: a perch + a TCP listener + a spool
//! drain.
//!
//! Startup ([`ReadyAgent::start`]) is the REQ-MSG-3 contract:
//! 1. Create the perch dir; write `info.json` (`ready_agent` endpoint) + the
//!    `ready` marker.
//! 2. Bind a loopback listener and register its address (via [`PollListener`]).
//! 3. Drain the **non-deferred** spool backlog so messages that arrived while
//!    the agent was offline are delivered on startup. Deferred rows are left
//!    for the hook channel (REQ-HAZARD-DEFERRED-SURVIVE-DRAIN, covered M0).
//!
//! Teardown ([`ReadyAgent::soft_cleanup`]) honors REQ-HAZARD-SOFT-CLEANUP (6.2):
//! it removes **only** the `ready` marker and unregisters the address. The
//! spool and `info.json` are preserved so a restart resumes its backlog.
// [impl->REQ-MSG-3]

use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;

use crate::listener::PollListener;

/// Default poll window for [`ReadyAgent::recv`]; bounds each blocking accept so
/// the caller stays responsive (and can re-check spool / shut down between calls).
const RECV_POLL: Duration = Duration::from_millis(200);

/// A running ready agent: an owned perch + a registered loopback listener.
pub struct ReadyAgent {
    id: String,
    listener: PollListener,
    perch_path: PathBuf,
    closed: bool,
}

impl ReadyAgent {
    /// Register the perch (info.json + ready marker + listener address) and
    /// drain the offline backlog. Returns the agent and any backlog messages
    /// (oldest first) that were spooled while it was down.
    // [impl->REQ-MSG-3]
    pub fn start(id: &str) -> Result<(ReadyAgent, Vec<String>), String> {
        // [impl->REQ-HAZARD-ID-CHARSET] guard the id charset at the creation seam.
        spt_proto::id::validate_endpoint_id(id).map_err(|e| e.to_string())?;
        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
        fs::create_dir_all(&perch_path)
            .map_err(|e| format!("Failed to create perch dir: {}", e))?;

        // info.json first, so the record exists by the time the address is live.
        let rec = InfoJson::new(id, &now_stamp(), std::process::id(), &session_id(), "ready_agent");
        info::write_info(&perch_path, &rec)
            .map_err(|e| format!("Failed to write info.json: {}", e))?;

        // ready marker — the liveness signal senders check.
        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
            .map_err(|e| format!("Failed to write ready marker: {}", e))?;

        // Bind + register the listener address. Blocking with a short poll so
        // recv() stays responsive.
        let listener = PollListener::bind_blocking(id, RECV_POLL)?;

        // Drain the offline backlog (non-deferred only; deferred rows survive
        // for the hook channel).
        let backlog = spool::drain_non_deferred_at(&perch_path)
            .map_err(|e| format!("Failed to drain spool backlog: {}", e))?;

        Ok((
            ReadyAgent {
                id: id.to_string(),
                listener,
                perch_path,
                closed: false,
            },
            backlog,
        ))
    }

    /// The agent's id.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Wait up to one poll window for a live TCP message. Returns `None` on
    /// timeout (call again to keep listening).
    pub fn recv(&self) -> Option<String> {
        self.listener.check_message_blocking()
    }

    /// Drain any non-deferred spool rows that accrued since startup (e.g. a
    /// sender that spooled because it raced the listener bind, or a hook-channel
    /// arrival). Oldest first.
    pub fn drain_spool(&self) -> Vec<String> {
        spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default()
    }

    /// Soft teardown (6.2): remove the `ready` marker and unregister the
    /// address. The spool and `info.json` are **preserved** so a later restart
    /// resumes its backlog. Idempotent.
    // [impl->REQ-MSG-3]
    // [impl->REQ-HAZARD-SOFT-CLEANUP]
    pub fn soft_cleanup(&mut self) {
        let _ = fs::remove_file(perch::resolve_ready_file(&self.id, ParentHint::Infer));
        self.listener.close();
        self.closed = true;
    }
}

impl Drop for ReadyAgent {
    fn drop(&mut self) {
        if !self.closed {
            self.soft_cleanup();
        }
    }
}

/// Coarse creation timestamp (epoch seconds as a string). Opaque to readers —
/// `info.json` treats `started` as a producer-format string.
fn now_stamp() -> String {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".to_string())
}

/// Harness session id: honor `OWL_SESSION_ID` if set, else synthesize a
/// process-unique fallback. (M2 wires the real harness session id.)
fn session_id() -> String {
    if let Ok(s) = std::env::var("OWL_SESSION_ID") {
        if !s.is_empty() {
            return s;
        }
    }
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    format!("sess-{}-{}", std::process::id(), nanos)
}

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

    fn unique_id(prefix: &str) -> String {
        let nanos = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0);
        format!("{}-{}-{}", prefix, std::process::id(), nanos)
    }

    // [unit->REQ-MSG-3] start registers a reachable, resolvable perch.
    #[test]
    fn start_registers_resolvable_perch() {
        crate::test_home::isolate();
        let id = unique_id("test-ready-start");
        let (agent, backlog) = ReadyAgent::start(&id).unwrap();
        assert!(backlog.is_empty());

        // Resolvable + online from a sender's point of view.
        assert!(deliver::is_online(&id));
        assert!(deliver::perch_exists(&id));

        // info.json + ready marker on disk.
        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
        assert!(info::read_info(&perch_path).is_some());
        assert!(perch::resolve_ready_file(&id, ParentHint::Infer).exists());
        drop(agent);
    }

    // [unit->REQ-MSG-3] backlog spooled while offline drains on startup.
    #[test]
    fn backlog_drains_on_start() {
        crate::test_home::isolate();
        let owlery = perch::owlery_dir();
        let id = unique_id("test-ready-backlog");
        // Pre-create an offline perch and spool a message to it.
        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
        fs::create_dir_all(&perch_path).unwrap();
        let rec = InfoJson::new(&id, "now", std::process::id(), "sess", "ready_agent");
        info::write_info(&perch_path, &rec).unwrap();
        assert_eq!(deliver::deliver(&id, "sender", "while you slept", &owlery), deliver::DeliveryPath::Spool);

        // Starting drains it.
        let (_agent, backlog) = ReadyAgent::start(&id).unwrap();
        assert_eq!(backlog.len(), 1);
        assert!(backlog[0].contains("while you slept"));
    }

    // [unit->REQ-MSG-3] a live TCP message is received via recv().
    #[test]
    fn recv_gets_live_tcp_message() {
        crate::test_home::isolate();
        let owlery = perch::owlery_dir();
        let id = unique_id("test-ready-recv");
        let (agent, _) = ReadyAgent::start(&id).unwrap();

        let owlery2 = owlery.clone();
        let target = id.clone();
        let h = std::thread::spawn(move || {
            std::thread::sleep(Duration::from_millis(30));
            deliver::deliver(&target, "peer", "hello live", &owlery2)
        });

        let mut got = None;
        for _ in 0..25 {
            if let Some(m) = agent.recv() {
                got = Some(m);
                break;
            }
        }
        assert_eq!(h.join().unwrap(), deliver::DeliveryPath::Tcp);
        assert_eq!(got.as_deref(), Some("__REPLY_TO__:peer\nhello live\n"));
        drop(agent);
    }

    // [unit->REQ-HAZARD-SOFT-CLEANUP] soft_cleanup removes only the ready marker;
    // spool (deferred row) and info.json are preserved.
    #[test]
    fn soft_cleanup_preserves_spool_and_info() {
        crate::test_home::isolate();
        let id = unique_id("test-ready-soft");
        let (mut agent, _) = ReadyAgent::start(&id).unwrap();
        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);

        // A deferred row survives the startup drain — use it to prove the spool
        // is preserved across cleanup.
        spool::spool_message_deferred_at(&perch_path, "hooker", "deferred body").unwrap();
        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);

        agent.soft_cleanup();

        // ready marker gone...
        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());
        // ...but spool row + info.json preserved.
        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
        assert!(info::read_info(&perch_path).is_some());
    }
}

+    /// [`start`](Self::start) with an explicit home-subnet pick for a NEW

+    /// endpoint (D9-5, REQ-INST-15): home is assigned at creation through the

+    /// shared seam ([`spt_store::home::stamp_creation_fields`]) — auto on a

+    /// sole-subnet node, `subnet` required on a multi-subnet one

+    /// (refuse-and-qualify), local-only when unpaired. A restart carries the

+    /// durable creation-time fields forward (home is immutable; the resource

+    /// blurb and knobs are user state a re-register must not wipe).

+    ) -> Result<(ReadyAgent, Vec<String>), String> {

-        let rec = InfoJson::new(id, &now_stamp(), std::process::id(), &session_id(), "ready_agent");

+        let prior = info::read_info(&perch_path);

+            InfoJson::new(id, &now_stamp(), std::process::id(), &session_id(), "ready_agent");

+        let subnets = spt_store::subnet::SubnetStore::load();

+        let mut vis = spt_store::visibility::VisibilityStore::load();

+            spt_store::home::stamp_creation_fields(&mut rec, prior.as_ref(), &subnets, &mut vis, subnet, None)

+                .map_err(|e| format!("HOME: {e}"))?;


startup.rs — establish_perch creation seam:


Edit

#[derive(Debug, PartialEq, Eq)]
pub enum BindError {
    NoSeed(u32),
    /// The seed's anchor process is dead → recycled/abandoned (5.1).
    StaleSeed(u32),
    EmptySession,
    /// A *live* perch already holds this id under a different session (5.1).
    Conflict { id: String, held: String },
    Io(String),
}

#[derive(Debug, PartialEq, Eq)]
pub enum BindError {
    NoSeed(u32),
    /// The seed's anchor process is dead → recycled/abandoned (5.1).
    StaleSeed(u32),
    EmptySession,
    /// A *live* perch already holds this id under a different session (5.1).
    Conflict { id: String, held: String },
    /// Home assignment refused for a NEW endpoint (REQ-INST-15:
    /// refuse-and-qualify on a multi-subnet node — no silent guess).
    Home(String),
    Io(String),
}


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)

#[derive(Debug, PartialEq, Eq)]
pub enum BindError {
    NoSeed(u32),
    /// The seed's anchor process is dead → recycled/abandoned (5.1).
    StaleSeed(u32),
    EmptySession,
    /// A *live* perch already holds this id under a different session (5.1).
    Conflict { id: String, held: String },
    Io(String),
}

#[derive(Debug, PartialEq, Eq)]
pub enum BindError {
    NoSeed(u32),
    /// The seed's anchor process is dead → recycled/abandoned (5.1).
    StaleSeed(u32),
    EmptySession,
    /// A *live* perch already holds this id under a different session (5.1).
    Conflict { id: String, held: String },
    /// Home assignment refused for a NEW endpoint (REQ-INST-15:
    /// refuse-and-qualify on a multi-subnet node — no silent guess).
    Home(String),
    Io(String),
}

+    /// Home assignment refused for a NEW endpoint (REQ-INST-15:

+    /// refuse-and-qualify on a multi-subnet node — no silent guess).


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

pub fn bind_from_seed(
    id: &str,
    parent_pid: u32,
    subnet: Option<&str>,
) -> 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));
    }
    // The seed carries the creating adapter — the adapter-at-creation rule's
    // source on this path (REQ-INST-15).
    let adapter = Some(seed.adapter.as_str()).filter(|a| !a.is_empty());
    establish_perch(id, &seed.session_id, seed.parent_pid, adapter, subnet)
}


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

pub fn bind_from_seed(
    id: &str,
    parent_pid: u32,
    subnet: Option<&str>,
) -> 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));
    }
    // The seed carries the creating adapter — the adapter-at-creation rule's
    // source on this path (REQ-INST-15).
    let adapter = Some(seed.adapter.as_str()).filter(|a| !a.is_empty());
    establish_perch(id, &seed.session_id, seed.parent_pid, adapter, subnet)
}

-pub fn bind_from_seed(id: &str, parent_pid: u32) -> Result<String, BindError> {

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

+    // The seed carries the creating adapter — the adapter-at-creation rule's

+    // source on this path (REQ-INST-15).

+    let adapter = Some(seed.adapter.as_str()).filter(|a| !a.is_empty());

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

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


/// 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).
fn establish_perch(id: &str, session_id: &str, parent_pid: u32) -> Result<String, BindError> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

    if let Some(existing) = info::read_info(&perch_path) {
        let owner_alive = info::read_pid(&perch_path)
            .map(proc::is_process_alive)
            .unwrap_or(false);
        if owner_alive
            && !existing.session_id.is_empty()
            && existing.session_id != session_id
        {
            return Err(BindError::Conflict {
                id: id.to_string(),
                held: existing.session_id,
            });
        }
    }

    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;
    let mut rec = InfoJson::new(id, &now_stamp(), std::process::id(), session_id, "live_agent");
    rec.parent_pid = Some(parent_pid);
    info::write_info(&perch_path, &rec).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).
///
/// Creation-time fields ride the shared seam
/// ([`spt_store::home::stamp_creation_fields`], REQ-INST-15): a NEW perch
/// assigns its immutable home (refuse-and-qualify on a multi-subnet node) +
/// seeds `sync_subnets = [home]`; a revive **carries forward** home, the
/// adapter (an explicit one wins — resume-under-new), the resource blurb,
/// and the auto-suspend override — a re-bind rewrites `info.json` and must
/// not wipe them.
// [impl->REQ-INST-15]
fn establish_perch(
    id: &str,
    session_id: &str,
    parent_pid: u32,
    adapter: Option<&str>,
    subnet: Option<&str>,
) -> Result<String, BindError> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

    let prior = info::read_info(&perch_path);
    if let Some(existing) = &prior {
        let owner_alive = info::read_pid(&perch_path)
            .map(proc::is_process_alive)
            .unwrap_or(false);
        if owner_alive
            && !existing.session_id.is_empty()
            && existing.session_id != session_id
        {
            return Err(BindError::Conflict {
                id: id.to_string(),
                held: existing.session_id.clone(),
            });
        }
    }

    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;
    let mut rec = InfoJson::new(id, &now_stamp(), std::process::id(), session_id, "live_agent");
    rec.parent_pid = Some(parent_pid);
    let subnets = spt_store::subnet::SubnetStore::load();
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let dirty = spt_store::home::stamp_creation_fields(
        &mut rec,
        prior.as_ref(),
        &subnets,
        &mut vis,
        subnet,
        adapter,
    )
    .map_err(|e| BindError::Home(e.to_string()))?;
    if dirty {
        let _ = vis.save();
    }
    info::write_info(&perch_path, &rec).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).
fn establish_perch(id: &str, session_id: &str, parent_pid: u32) -> Result<String, BindError> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

    if let Some(existing) = info::read_info(&perch_path) {
        let owner_alive = info::read_pid(&perch_path)
            .map(proc::is_process_alive)
            .unwrap_or(false);
        if owner_alive
            && !existing.session_id.is_empty()
            && existing.session_id != session_id
        {
            return Err(BindError::Conflict {
                id: id.to_string(),
                held: existing.session_id,
            });
        }
    }

    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;
    let mut rec = InfoJson::new(id, &now_stamp(), std::process::id(), session_id, "live_agent");
    rec.parent_pid = Some(parent_pid);
    info::write_info(&perch_path, &rec).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).
///
/// Creation-time fields ride the shared seam
/// ([`spt_store::home::stamp_creation_fields`], REQ-INST-15): a NEW perch
/// assigns its immutable home (refuse-and-qualify on a multi-subnet node) +
/// seeds `sync_subnets = [home]`; a revive **carries forward** home, the
/// adapter (an explicit one wins — resume-under-new), the resource blurb,
/// and the auto-suspend override — a re-bind rewrites `info.json` and must
/// not wipe them.
// [impl->REQ-INST-15]
fn establish_perch(
    id: &str,
    session_id: &str,
    parent_pid: u32,
    adapter: Option<&str>,
    subnet: Option<&str>,
) -> Result<String, BindError> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

    let prior = info::read_info(&perch_path);
    if let Some(existing) = &prior {
        let owner_alive = info::read_pid(&perch_path)
            .map(proc::is_process_alive)
            .unwrap_or(false);
        if owner_alive
            && !existing.session_id.is_empty()
            && existing.session_id != session_id
        {
            return Err(BindError::Conflict {
                id: id.to_string(),
                held: existing.session_id.clone(),
            });
        }
    }

    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;
    let mut rec = InfoJson::new(id, &now_stamp(), std::process::id(), session_id, "live_agent");
    rec.parent_pid = Some(parent_pid);
    let subnets = spt_store::subnet::SubnetStore::load();
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let dirty = spt_store::home::stamp_creation_fields(
        &mut rec,
        prior.as_ref(),
        &subnets,
        &mut vis,
        subnet,
        adapter,
    )
    .map_err(|e| BindError::Home(e.to_string()))?;
    if dirty {
        let _ = vis.save();
    }
    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;

-fn establish_perch(id: &str, session_id: &str, parent_pid: u32) -> Result<String, BindError> {

+/// Creation-time fields ride the shared seam

+/// ([`spt_store::home::stamp_creation_fields`], REQ-INST-15): a NEW perch

+/// assigns its immutable home (refuse-and-qualify on a multi-subnet node) +

+/// seeds `sync_subnets = [home]`; a revive **carries forward** home, the

+/// adapter (an explicit one wins — resume-under-new), the resource blurb,

+/// and the auto-suspend override — a re-bind rewrites `info.json` and must

-    if let Some(existing) = info::read_info(&perch_path) {

+    let prior = info::read_info(&perch_path);

-                held: existing.session_id,

+                held: existing.session_id.clone(),

+    let subnets = spt_store::subnet::SubnetStore::load();

+    let mut vis = spt_store::visibility::VisibilityStore::load();

+    let dirty = spt_store::home::stamp_creation_fields(

+    .map_err(|e| BindError::Home(e.to_string()))?;


crates\spt\src\api\live.rs:8://! This module keeps the binary's call sites stable — `cmd_listen` /
crates\spt\src\api\mod.rs:239:            startup::cmd_listen(&id, parent_pid, once, ctx.manifest.as_ref())
crates\spt\src\api\mod.rs:242:            startup::cmd_bind(&id, bind_session, auth.session_id)
crates\spt\src\api\startup.rs:100:pub fn bind_from_seed(
crates\spt\src\api\startup.rs:118:    establish_perch(id, &seed.session_id, seed.parent_pid, adapter, subnet)
crates\spt\src\api\startup.rs:142:fn establish_perch(
crates\spt\src\api\startup.rs:205:pub fn cmd_listen(
crates\spt\src\api\startup.rs:223:    let token = match bind_from_seed(id, parent_pid) {
crates\spt\src\api\startup.rs:226:            report_bind_error(&e);
crates\spt\src\api\startup.rs:304:pub fn cmd_bind(id: &str, set_session_id: Option<String>, proof_session: Option<String>) -> i32 {
crates\spt\src\api\startup.rs:320:    match establish_perch(id, &session_id, parent_pid) {
crates\spt\src\api\startup.rs:329:            report_bind_error(&e);
crates\spt\src\api\startup.rs:335:fn report_bind_error(e: &BindError) {
crates\spt\src\api\startup.rs:411:        let token = bind_from_seed("alice", anchor).expect("bind");
crates\spt\src\api\startup.rs:430:        let err = bind_from_seed("alice", DEAD).unwrap_err();
crates\spt\src\api\startup.rs:441:        let err = bind_from_seed("alice", std::process::id()).unwrap_err();
crates\spt\src\api\startup.rs:451:        assert_eq!(bind_from_seed("alice", anchor).unwrap_err(), BindError::EmptySession);
crates\spt\src\api\startup.rs:462:        bind_from_seed("alice", anchor).unwrap();
crates\spt\src\api\startup.rs:465:        let err = bind_from_seed("alice", anchor).unwrap_err();
crates\spt\src\api\startup.rs:472:        let code = cmd_bind("bob", Some("sid-boot".into()), None);
crates\spt\src\api\startup.rs:482:        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None), 0);
crates\spt\src\api\startup.rs:483:        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None), 0);


295	        pulse(&live);
296	    }
297	}
298
299	/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the
300	/// live perch on first contact, or flips an existing skeleton, keyed to the
301	/// session id the binary reports. Auth is intrinsic: an existing *live* perch
302	/// under a different session id is refused (you can only bind your own).
303	// [impl->REQ-SEAM-POSTSPAWN]
304	pub fn cmd_bind(id: &str, set_session_id: Option<String>, proof_session: Option<String>) -> i32 {
305	    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
306	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
307	        eprintln!("INVALID_ID:{id}: {e}");
308	        return EXIT_REFUSED;
309	    }
310	    let session_id = set_session_id
311	        .or(proof_session)
312	        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
313	        .filter(|s| !s.is_empty());
314	    let Some(session_id) = session_id else {
315	        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
316	        return EXIT_REFUSED;
317	    };
318	    // The session's own anchor: the process that fired this hook (the session).
319	    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
320	    match establish_perch(id, &session_id, parent_pid) {
321	        Ok(token) => {
322	            eprintln!("BOUND:{id} token={token}");
323	            // A post-spawn bind is a new-session-start — a reported resurface
324	            // boundary (ADR-0007/D8b). Best-effort.
325	            super::reporting::resurface_notifs(id);
326	            0
327	        }
328	        Err(e) => {
329	            report_bind_error(&e);
330	            EXIT_REFUSED
331	        }
332	    }
333	}
334
335	fn report_bind_error(e: &BindError) {
336	    match e {
337	        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
338	        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),
339	        BindError::EmptySession => eprintln!("EMPTY_SESSION: seed carried no session id"),
340	        BindError::Conflict { id, held } => {
341	            eprintln!("CONFLICT:{id} is live under session {held}")
342	        }
343	        BindError::Io(m) => eprintln!("BIND_IO: {m}"),
344	    }
345	}
346
347	/// Print a framed message to stdout and flush (one delivery per emit).
348	fn emit(msg: &str) {
349	    use std::io::Write;
350	    print!("{msg}");
351	    let _ = std::io::stdout().flush();
352	}
353
354	/// Coarse creation timestamp (epoch seconds as a string), matching the
355	/// `info.json` `started` producer-format convention.
356	fn now_stamp() -> String {
357	    std::time::SystemTime::now()
358	        .duration_since(std::time::UNIX_EPOCH)
359	        .map(|d| d.as_secs().to_string())
360	        .unwrap_or_else(|_| "0".to_string())
361	}
362
363	#[cfg(test)]
364	mod tests {
365	    use super::*;
366	    use crate::testutil::isolated_home;
367	    use spt_daemon::SeedRegistry;
368	    use std::sync::Arc;
369	    use std::time::Duration;

        pulse(&live);
    }
}

/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the
/// live perch on first contact, or flips an existing skeleton, keyed to the
/// session id the binary reports. Auth is intrinsic: an existing *live* perch
/// under a different session id is refused (you can only bind your own).
// [impl->REQ-SEAM-POSTSPAWN]
pub fn cmd_bind(id: &str, set_session_id: Option<String>, proof_session: Option<String>) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }
        Err(e) => {
            report_bind_error(&e);
            EXIT_REFUSED
        }
    }
}

fn report_bind_error(e: &BindError) {
    match e {
        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),
        BindError::EmptySession => eprintln!("EMPTY_SESSION: seed carried no session id"),
        BindError::Conflict { id, held } => {
            eprintln!("CONFLICT:{id} is live under session {held}")
        }
        BindError::Io(m) => eprintln!("BIND_IO: {m}"),
    }
}

/// Print a framed message to stdout and flush (one delivery per emit).
fn emit(msg: &str) {
    use std::io::Write;
    print!("{msg}");
    let _ = std::io::stdout().flush();
}

/// Coarse creation timestamp (epoch seconds as a string), matching the
/// `info.json` `started` producer-format convention.
fn now_stamp() -> String {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".to_string())
}

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


pub fn cmd_bind(id: &str, set_session_id: Option<String>, proof_session: Option<String>) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid) {

pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid, adapter, subnet) {


pub fn cmd_bind(id: &str, set_session_id: Option<String>, proof_session: Option<String>) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid) {

pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid, adapter, subnet) {

/// session id the binary reports. Auth is intrinsic: an existing *live* perch

/// under a different session id is refused (you can only bind your own).

-pub fn cmd_bind(id: &str, set_session_id: Option<String>, proof_session: Option<String>) -> i32 {

-    match establish_perch(id, &session_id, parent_pid) {

+    match establish_perch(id, &session_id, parent_pid, adapter, subnet) {

// A post-spawn bind is a new-session-start — a reported resurface


        BindError::Conflict { id, held } => {
            eprintln!("CONFLICT:{id} is live under session {held}")
        }
        BindError::Io(m) => eprintln!("BIND_IO: {m}"),
    }
}

        BindError::Conflict { id, held } => {
            eprintln!("CONFLICT:{id} is live under session {held}")
        }
        BindError::Home(m) => eprintln!("HOME_REFUSED: {m}"),
        BindError::Io(m) => eprintln!("BIND_IO: {m}"),
    }
}


        BindError::Conflict { id, held } => {
            eprintln!("CONFLICT:{id} is live under session {held}")
        }
        BindError::Io(m) => eprintln!("BIND_IO: {m}"),
    }
}

        BindError::Conflict { id, held } => {
            eprintln!("CONFLICT:{id} is live under session {held}")
        }
        BindError::Home(m) => eprintln!("HOME_REFUSED: {m}"),
        BindError::Io(m) => eprintln!("BIND_IO: {m}"),
    }
}

+        BindError::Home(m) => eprintln!("HOME_REFUSED: {m}"),


pub fn cmd_listen(
    id: &str,
    parent_pid_override: Option<u32>,
    once: bool,
    manifest: Option<&spt_runtime::Manifest>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {
        Some(p) => p,
        None => {
            eprintln!("NO_PARENT_PID: could not resolve the parent pid anchor");
            return EXIT_REFUSED;
        }
    };
    let token = match bind_from_seed(id, parent_pid) {

pub fn cmd_listen(
    id: &str,
    parent_pid_override: Option<u32>,
    once: bool,
    manifest: Option<&spt_runtime::Manifest>,
    subnet: Option<&str>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {
        Some(p) => p,
        None => {
            eprintln!("NO_PARENT_PID: could not resolve the parent pid anchor");
            return EXIT_REFUSED;
        }
    };
    let token = match bind_from_seed(id, parent_pid, subnet) {


pub fn cmd_listen(
    id: &str,
    parent_pid_override: Option<u32>,
    once: bool,
    manifest: Option<&spt_runtime::Manifest>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {
        Some(p) => p,
        None => {
            eprintln!("NO_PARENT_PID: could not resolve the parent pid anchor");
            return EXIT_REFUSED;
        }
    };
    let token = match bind_from_seed(id, parent_pid) {

pub fn cmd_listen(
    id: &str,
    parent_pid_override: Option<u32>,
    once: bool,
    manifest: Option<&spt_runtime::Manifest>,
    subnet: Option<&str>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {
        Some(p) => p,
        None => {
            eprintln!("NO_PARENT_PID: could not resolve the parent pid anchor");
            return EXIT_REFUSED;
        }
    };
    let token = match bind_from_seed(id, parent_pid, subnet) {

-    let token = match bind_from_seed(id, parent_pid) {

+    let token = match bind_from_seed(id, parent_pid, subnet) {


D9-3: dormancy budget measurement + DORMANCY-BUDGET.md + ADR-0003 #9 close

SPT_BUDGET=1 harness (done, uncommitted). HFENDULEAM runs done (shell 7.7MiB, claude 308MiB, zero idle CPU). Remaining: gravity-linux leg (workflow_dispatch budget job on gravity runner), docs/DORMANCY-BUDGET.md (methodology + numbers + policy lock, doc->REQ-INST-3), ADR-0003 #9 amendment, CONTEXT.md policy confirm. CI green.

D9-4: REQ-INST-14 resource advertisement (blurb on Instance + spt resources CLI + projection)

resources: Option<String> on Instance (serde-default, rides RegistryUpdate + lease); resource_projection over visible+routable rows; info.json resources field + set_resources; daemon.json resources_blurb node seed (drop Copy on DaemonConfig); advertise_local fills endpoint-else-node blurb; spt resources set/show/list CLI; CONTEXT doc tag; activate REQ-INST-14 [doc, impl, unit]. CI green.

D9-5: REQ-INST-15 immutable home subnet + spt fork

D9-5-1: home model (info.json home_subnet+adapter, assign_home matrix, creation-seam stamping + carry-forward on revive, sync_subnets=[home] seed, first-join adoption via CLI create + advertise sweep). D9-5-2: spt fork (ContextStore::fork_endpoint seed-commit copy both tiers, join-time collision vs target snapshot, new perch home=target, --delete-source). ADR-0010 doc tags; activate REQ-INST-15 [doc, impl, unit].


Fix startup.rs test call sites + api/mod.rs dispatch:


395	            }
396	            std::thread::sleep(Duration::from_millis(5));
397	        }
398	        panic!("seed daemon did not come up");
399	    }
400
401	    fn put_seed(reg: &SeedRegistry, parent_pid: u32, session_id: &str) {
402	        reg.put(Seed {
403	            parent_pid,
404	            session_id: session_id.into(),
405	            adapter: "mock".into(),
406	            cwd: None,
407	            created: "0".into(),
408	        });
409	    }
410
411	    // [unit->REQ-START-2] harness-hosted startup: a seed → listen binds the perch.
412	    #[test] // [unit->REQ-START-3] a valid seed (live anchor) binds a live perch.
413	    fn valid_seed_binds_live_perch() {
414	        let _h = isolated_home();
415	        let reg = start_seed_daemon();
416	        // Use this very process as the (alive) anchor.
417	        let anchor = std::process::id();
418	        put_seed(&reg, anchor, "sid-1");
419	        let token = bind_from_seed("alice", anchor).expect("bind");
420	        assert_eq!(token.len(), 32);
421
422	        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
423	        let rec = info::read_info(&perch_path).expect("info.json");
424	        assert_eq!(rec.session_id, "sid-1");
425	        assert_eq!(rec.state, "live_agent");
426	        assert_eq!(rec.parent_pid, Some(anchor));
427	        assert!(perch::resolve_ready_file("alice", ParentHint::Infer).exists());
428	        // Seed is consumed from the daemon's in-memory map (take-once).
429	        assert!(reg.peek(anchor).is_none());
430	    }
431
432	    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a dead-anchor seed never binds.
433	    fn stale_seed_with_dead_anchor_refused() {
434	        let _h = isolated_home();
435	        let reg = start_seed_daemon();
436	        const DEAD: u32 = 2_000_000_000;
437	        put_seed(&reg, DEAD, "sid-1");
438	        let err = bind_from_seed("alice", DEAD).unwrap_err();
439	        assert_eq!(err, BindError::StaleSeed(DEAD));
440	        // No perch was created.
441	        assert!(info::read_info(&perch::resolve_perch_path("alice", ParentHint::Infer)).is_none());
442	    }
443
444	    // [unit->REQ-START-2] no seed for this listener → bind is refused, not bound.
445	    #[test] // [unit->REQ-START-3] absent seed is refused, not bound.
446	    fn missing_seed_refused() {
447	        let _h = isolated_home();
448	        let _reg = start_seed_daemon();
449	        let err = bind_from_seed("alice", std::process::id()).unwrap_err();
450	        assert!(matches!(err, BindError::NoSeed(_)));
451	    }
452
453	    #[test] // [unit->REQ-START-3] empty session id in the seed is refused.
454	    fn empty_session_seed_refused() {
455	        let _h = isolated_home();
456	        let reg = start_seed_daemon();
457	        let anchor = std::process::id();
458	        put_seed(&reg, anchor, "");
459	        assert_eq!(bind_from_seed("alice", anchor).unwrap_err(), BindError::EmptySession);
460	    }
461
462	    #[test] // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a live perch under another
463	    // session is not silently rebound (id-collision guard).
464	    fn live_perch_under_other_session_conflicts() {
465	        let _h = isolated_home();
466	        let reg = start_seed_daemon();
467	        let anchor = std::process::id();
468	        // First bind establishes alice@sid-1 owned by this (live) process.
469	        put_seed(&reg, anchor, "sid-1");
470	        bind_from_seed("alice", anchor).unwrap();
471	        // A second seed tries to claim alice for a different session.
472	        put_seed(&reg, anchor, "sid-2");
473	        let err = bind_from_seed("alice", anchor).unwrap_err();
474	        assert_eq!(err, BindError::Conflict { id: "alice".into(), held: "sid-1".into() });
475	    }
476
477	    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
478	    fn post_spawn_bind_establishes_perch() {
479	        let _h = isolated_home();
480	        let code = cmd_bind("bob", Some("sid-boot".into()), None);
481	        assert_eq!(code, 0);
482	        let rec = info::read_info(&perch::resolve_perch_path("bob", ParentHint::Infer)).unwrap();
483	        assert_eq!(rec.session_id, "sid-boot");
484	        assert_eq!(rec.state, "live_agent");
485	    }
486
487	    #[test] // [unit->REQ-SEAM-POSTSPAWN] re-bind with the same session is idempotent.
488	    fn rebind_same_session_ok() {
489	        let _h = isolated_home();
490	        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None), 0);
491	        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None), 0);
492	    }
493	}
494

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

    // [unit->REQ-START-2] harness-hosted startup: a seed → listen binds the perch.
    #[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());
    }

    // [unit->REQ-START-2] no seed for this listener → bind is refused, not bound.
    #[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() });
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
    fn post_spawn_bind_establishes_perch() {
        let _h = isolated_home();
        let code = cmd_bind("bob", Some("sid-boot".into()), None);
        assert_eq!(code, 0);
        let rec = info::read_info(&perch::resolve_perch_path("bob", ParentHint::Infer)).unwrap();
        assert_eq!(rec.session_id, "sid-boot");
        assert_eq!(rec.state, "live_agent");
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] re-bind with the same session is idempotent.
    fn rebind_same_session_ok() {
        let _h = isolated_home();
        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None), 0);
        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None), 0);
    }
}


PowerShell

(Get-Content "crates\spt\src\api\startup.rs" -Raw) -replace 'bind_from_seed\("alice", anchor\)', 'bind_from_seed("alice", anchor, None)' -replace 'bind_from_seed\("alice", DEAD\)', 'bind_from_seed("alice", DEAD, None)' -replace 'bind_from_seed\("alice", std::process::id\(\)\)', 'bind_from_seed("alice", std::process::id(), None)' -replace 'cmd_bind\("bob", Some\("sid-boot"\.into\(\)\), None\)', 'cmd_bind("bob", Some("sid-boot".into()), None, None, None)' | Set-Content "crates\spt\src\api\startup.rs" -NoNewline

Update test call sites in startup.rs


(PowerShell completed with no output)


Add home test to startup.rs:


Edit

#[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.

    // [unit->REQ-INST-15] home rides the bind seam: a NEW endpoint on a
    // sole-subnet node homes there (+ the seed's adapter records, + the sync
    // scope seeds); the RE-BIND — which rewrites info.json — carries home
    // forward (immutable); a NEW endpoint on a multi-subnet node refuses
    // without an explicit pick and binds with one.
    #[test]
    fn bind_assigns_home_and_revive_preserves_it() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        let anchor = std::process::id();

        let mut subnets = spt_store::subnet::SubnetStore::load();
        subnets.create_subnet("home").unwrap();
        subnets.save().unwrap();

        put_seed(&reg, anchor, "sid-1");
        bind_from_seed("alice", anchor, None).expect("bind");
        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
        let rec = info::read_info(&perch_path).unwrap();
        assert_eq!(rec.home_subnet.as_deref(), Some("home"), "sole subnet auto-homes");
        assert_eq!(rec.adapter.as_deref(), Some("mock"), "the seed's adapter records");
        let vis = spt_store::visibility::VisibilityStore::load();
        assert_eq!(
            vis.sync_subnets("alice"),
            Some(&["home".to_string()][..]),
            "creation seeds sync_subnets = [home] (the visibility seam, now real)"
        );

        // Re-bind rewrites info.json — home survives by carry-forward.
        put_seed(&reg, anchor, "sid-1");
        bind_from_seed("alice", anchor, None).expect("re-bind");
        assert_eq!(
            info::read_info(&perch_path).unwrap().home_subnet.as_deref(),
            Some("home"),
            "home is immutable across the re-bind"
        );

        // Multi-subnet node: a NEW endpoint refuses without an explicit pick.
        subnets.create_subnet("work").unwrap();
        subnets.save().unwrap();
        put_seed(&reg, anchor, "sid-9");
        let err = bind_from_seed("fresh", anchor, None).unwrap_err();
        assert!(matches!(err, BindError::Home(_)), "refuse-and-qualify: {err:?}");
        put_seed(&reg, anchor, "sid-9");
        bind_from_seed("fresh", anchor, Some("work")).expect("explicit pick binds");
        let fresh = perch::resolve_perch_path("fresh", ParentHint::Infer);
        assert_eq!(info::read_info(&fresh).unwrap().home_subnet.as_deref(), Some("work"));
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>

Error: File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.


470	        bind_from_seed("alice", anchor, None).unwrap();
471	        // A second seed tries to claim alice for a different session.
472	        put_seed(&reg, anchor, "sid-2");
473	        let err = bind_from_seed("alice", anchor, None).unwrap_err();
474	        assert_eq!(err, BindError::Conflict { id: "alice".into(), held: "sid-1".into() });
475	    }
476
477	    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
478	    fn post_spawn_bind_establishes_perch() {
479	        let _h = isolated_home();
480	        let code = cmd_bind("bob", Some("sid-boot".into()), None, None, None);
481	        assert_eq!(code, 0);
482	        let rec = info::read_info(&perch::resolve_perch_path("bob", ParentHint::Infer)).unwrap();
483	        assert_eq!(rec.session_id, "sid-boot");
484	        assert_eq!(rec.state, "live_agent");
485	    }
486
487	    #[test] // [unit->REQ-SEAM-POSTSPAWN] re-bind with the same session is idempotent.
488	    fn rebind_same_session_ok() {
489	        let _h = isolated_home();
490	        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None, None, None), 0);
491	        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None, None, None), 0);
492	    }
493	}
494

        bind_from_seed("alice", anchor, None).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, None).unwrap_err();
        assert_eq!(err, BindError::Conflict { id: "alice".into(), held: "sid-1".into() });
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
    fn post_spawn_bind_establishes_perch() {
        let _h = isolated_home();
        let code = cmd_bind("bob", Some("sid-boot".into()), None, None, None);
        assert_eq!(code, 0);
        let rec = info::read_info(&perch::resolve_perch_path("bob", ParentHint::Infer)).unwrap();
        assert_eq!(rec.session_id, "sid-boot");
        assert_eq!(rec.state, "live_agent");
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] re-bind with the same session is idempotent.
    fn rebind_same_session_ok() {
        let _h = isolated_home();
        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None, None, None), 0);
        assert_eq!(cmd_bind("bob", Some("sid-boot".into()), None, None, None), 0);
    }
}


    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
    fn post_spawn_bind_establishes_perch() {

    // [unit->REQ-INST-15] home rides the bind seam: a NEW endpoint on a
    // sole-subnet node homes there (+ the seed's adapter records, + the sync
    // scope seeds); the RE-BIND — which rewrites info.json — carries home
    // forward (immutable); a NEW endpoint on a multi-subnet node refuses
    // without an explicit pick and binds with one.
    #[test]
    fn bind_assigns_home_and_revive_preserves_it() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        let anchor = std::process::id();

        let mut subnets = spt_store::subnet::SubnetStore::load();
        subnets.create_subnet("home").unwrap();
        subnets.save().unwrap();

        put_seed(&reg, anchor, "sid-1");
        bind_from_seed("alice", anchor, None).expect("bind");
        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
        let rec = info::read_info(&perch_path).unwrap();
        assert_eq!(rec.home_subnet.as_deref(), Some("home"), "sole subnet auto-homes");
        assert_eq!(rec.adapter.as_deref(), Some("mock"), "the seed's adapter records");
        let vis = spt_store::visibility::VisibilityStore::load();
        assert_eq!(
            vis.sync_subnets("alice"),
            Some(&["home".to_string()][..]),
            "creation seeds sync_subnets = [home] (the visibility seam, now real)"
        );

        // Re-bind rewrites info.json — home survives by carry-forward.
        put_seed(&reg, anchor, "sid-1");
        bind_from_seed("alice", anchor, None).expect("re-bind");
        assert_eq!(
            info::read_info(&perch_path).unwrap().home_subnet.as_deref(),
            Some("home"),
            "home is immutable across the re-bind"
        );

        // Multi-subnet node: a NEW endpoint refuses without an explicit pick.
        subnets.create_subnet("work").unwrap();
        subnets.save().unwrap();
        put_seed(&reg, anchor, "sid-9");
        let err = bind_from_seed("fresh", anchor, None).unwrap_err();
        assert!(matches!(err, BindError::Home(_)), "refuse-and-qualify: {err:?}");
        put_seed(&reg, anchor, "sid-9");
        bind_from_seed("fresh", anchor, Some("work")).expect("explicit pick binds");
        let fresh = perch::resolve_perch_path("fresh", ParentHint::Infer);
        assert_eq!(info::read_info(&fresh).unwrap().home_subnet.as_deref(), Some("work"));
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
    fn post_spawn_bind_establishes_perch() {


    // [unit->REQ-INST-15] home rides the bind seam: a NEW endpoint on a
    // sole-subnet node homes there (+ the seed's adapter records, + the sync
    // scope seeds); the RE-BIND — which rewrites info.json — carries home
    // forward (immutable); a NEW endpoint on a multi-subnet node refuses
    // without an explicit pick and binds with one.
    #[test]
    fn bind_assigns_home_and_revive_preserves_it() {
        let _h = isolated_home();
        let reg = start_seed_daemon();
        let anchor = std::process::id();

        let mut subnets = spt_store::subnet::SubnetStore::load();
        subnets.create_subnet("home").unwrap();
        subnets.save().unwrap();

        put_seed(&reg, anchor, "sid-1");
        bind_from_seed("alice", anchor, None).expect("bind");
        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
        let rec = info::read_info(&perch_path).unwrap();
        assert_eq!(rec.home_subnet.as_deref(), Some("home"), "sole subnet auto-homes");
        assert_eq!(rec.adapter.as_deref(), Some("mock"), "the seed's adapter records");
        let vis = spt_store::visibility::VisibilityStore::load();
        assert_eq!(
            vis.sync_subnets("alice"),
            Some(&["home".to_string()][..]),
            "creation seeds sync_subnets = [home] (the visibility seam, now real)"
        );

        // Re-bind rewrites info.json — home survives by carry-forward.
        put_seed(&reg, anchor, "sid-1");
        bind_from_seed("alice", anchor, None).expect("re-bind");
        assert_eq!(
            info::read_info(&perch_path).unwrap().home_subnet.as_deref(),
            Some("home"),
            "home is immutable across the re-bind"
        );

        // Multi-subnet node: a NEW endpoint refuses without an explicit pick.
        subnets.create_subnet("work").unwrap();
        subnets.save().unwrap();
        put_seed(&reg, anchor, "sid-9");
        let err = bind_from_seed("fresh", anchor, None).unwrap_err();
        assert!(matches!(err, BindError::Home(_)), "refuse-and-qualify: {err:?}");
        put_seed(&reg, anchor, "sid-9");
        bind_from_seed("fresh", anchor, Some("work")).expect("explicit pick binds");
        let fresh = perch::resolve_perch_path("fresh", ParentHint::Infer);
        assert_eq!(info::read_info(&fresh).unwrap().home_subnet.as_deref(), Some("work"));
    }

    #[test] // [unit->REQ-SEAM-POSTSPAWN] first-contact bind establishes a live perch.
    fn post_spawn_bind_establishes_perch() {

assert_eq!(err, BindError::Conflict { id: "alice".into(), held: "sid-1".into() });

+    // [unit->REQ-INST-15] home rides the bind seam: a NEW endpoint on a

+    // sole-subnet node homes there (+ the seed's adapter records, + the sync

+    // scope seeds); the RE-BIND — which rewrites info.json — carries home

+    // forward (immutable); a NEW endpoint on a multi-subnet node refuses

+    // without an explicit pick and binds with one.

+    fn bind_assigns_home_and_revive_preserves_it() {

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

+        let mut subnets = spt_store::subnet::SubnetStore::load();

+        subnets.create_subnet("home").unwrap();

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

+        bind_from_seed("alice", anchor, None).expect("bind");

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

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

+        assert_eq!(rec.home_subnet.as_deref(), Some("home"), "sole subnet auto-homes");

+        assert_eq!(rec.adapter.as_deref(), Some("mock"), "the seed's adapter records");

+        let vis = spt_store::visibility::VisibilityStore::load();

+            "creation seeds sync_subnets = [home] (the visibility seam, now real)"

+        // Re-bind rewrites info.json — home survives by carry-forward.

+        bind_from_seed("alice", anchor, None).expect("re-bind");

+            info::read_info(&perch_path).unwrap().home_subnet.as_deref(),

+            "home is immutable across the re-bind"

+        // Multi-subnet node: a NEW endpoint refuses without an explicit pick.

+        subnets.create_subnet("work").unwrap();

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

+        let err = bind_from_seed("fresh", anchor, None).unwrap_err();

+        assert!(matches!(err, BindError::Home(_)), "refuse-and-qualify: {err:?}");

+        bind_from_seed("fresh", anchor, Some("work")).expect("explicit pick binds");

+        let fresh = perch::resolve_perch_path("fresh", ParentHint::Infer);

+        assert_eq!(info::read_info(&fresh).unwrap().home_subnet.as_deref(), Some("work"));


api/mod.rs flags + dispatch:


60	    /// Consume a seed and hold the perch + relay loop (interim long-runner, T4).
61	    Listen {
62	        id: String,
63	        /// Override the parent-pid anchor (defaults to the self-discovered PPID).
64	        #[arg(long)]
65	        parent_pid: Option<u32>,
66	        /// Drain backlog + one receive cycle, then exit (testability).
67	        #[arg(long)]
68	        once: bool,
69	    },
70	    /// Post-spawn bind of a session to its perch (T4).
71	    Bind {
72	        id: String,
73	        /// The session id discovered post-spawn, written into the perch record.
74	        #[arg(long = "set-session-id")]
75	        bind_session: Option<String>,
76	        #[command(flatten)]
77	        auth: AuthFlags,
78	    },
79	    /// Set activity state busy|idle (+ echo-gate sentinel) (T5).
80	    State {
81	        #[arg(value_parser = ["busy", "idle"])]
82	        state: String,
83	        id: String,
84	        #[arg(long)]
85	        no_gate: bool,
86	        #[command(flatten)]
87	        auth: AuthFlags,
88	    },
89	    /// Manage the echo-gate sentinel directly (T5).
90	    EchoGate {
91	        #[arg(value_parser = ["set", "clear"])]
92	        action: String,
93	        id: String,
94	        #[command(flatten)]
95	        auth: AuthFlags,
96	    },
97	    /// Drain delivered messages (hook channel) (T5).
98	    Poll {
99	        id: String,
100	        #[arg(long)]
101	        include_deferred: bool,
102	        #[command(flatten)]
103	        auth: AuthFlags,
104	    },
105	    /// Create a nested worker perch under a parent (T6).
106	    WorkerStart {
107	        parent: String,
108	        id: String,
109	        #[command(flatten)]
110	        auth: AuthFlags,
111	    },
112	    /// Tear down a worker perch (T6).
113	    WorkerStop {
114	        id: String,
115	        #[command(flatten)]
116	        auth: AuthFlags,
117	    },
118	    /// Drain a worker perch's messages (T6).
119	    WorkerPoll {
120	        id: String,
121	        #[command(flatten)]
122	        auth: AuthFlags,
123	    },
124	    /// Rebind the perch to a new session_id, preserving identity (T7).
125	    Boundary {
126	        #[arg(value_parser = ["clear", "compact"])]
127	        mode: String,
128	        id: String,
129	        /// The new session id to rebind the perch to.
130	        #[arg(long = "to-session-id")]
131	        to_session: String,
132	        #[command(flatten)]
133	        auth: AuthFlags,
134	    },
135	    /// Soft teardown (spool/history preserved); `--erase` hard-wipes (T7).
136	    SessionEnd {
137	        id: String,
138	        #[arg(long)]
139	        erase: bool,
140	        #[command(flatten)]
141	        auth: AuthFlags,
142	    },
143	    /// Report presence (T7).
144	    Presence {
145	        id: String,
146	        #[command(flatten)]
147	        auth: AuthFlags,
148	    },
149	    /// Report which node (if any) is remote-driving this endpoint (M4-D5c).
150	    DrivenBy {
151	        id: String,
152	        #[command(flatten)]
153	        auth: AuthFlags,
154	    },
155	    /// Append to the native history store (Path-B) (T7).
156	    HistoryLog {
157	        id: String,
158	        #[command(flatten)]
159	        auth: AuthFlags,
160	    },
161	    /// Emit a Shell sensory payload (stub) (T7).
162	    Emit {
163	        id: String,
164	        #[arg(long)]
165	        r#type: String,
166	        #[command(flatten)]
167	        auth: AuthFlags,
168	    },
169	    /// Print the adapter's declared capability (`hostable_types`) (T7).
170	    Capability,
171	    /// Graceful live-agent signoff (M2b): run the final echo-commune BEFORE
172	    /// teardown (3.3), then soft-stop. The `spt shutdown` lifecycle path.
173	    Shutdown {
174	        id: String,
175	        #[command(flatten)]
176	        auth: AuthFlags,
177	    },
178	}
179

    /// Consume a seed and hold the perch + relay loop (interim long-runner, T4).
    Listen {
        id: String,
        /// Override the parent-pid anchor (defaults to the self-discovered PPID).
        #[arg(long)]
        parent_pid: Option<u32>,
        /// Drain backlog + one receive cycle, then exit (testability).
        #[arg(long)]
        once: bool,
    },
    /// Post-spawn bind of a session to its perch (T4).
    Bind {
        id: String,
        /// The session id discovered post-spawn, written into the perch record.
        #[arg(long = "set-session-id")]
        bind_session: Option<String>,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Set activity state busy|idle (+ echo-gate sentinel) (T5).
    State {
        #[arg(value_parser = ["busy", "idle"])]
        state: String,
        id: String,
        #[arg(long)]
        no_gate: bool,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Manage the echo-gate sentinel directly (T5).
    EchoGate {
        #[arg(value_parser = ["set", "clear"])]
        action: String,
        id: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Drain delivered messages (hook channel) (T5).
    Poll {
        id: String,
        #[arg(long)]
        include_deferred: bool,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Create a nested worker perch under a parent (T6).
    WorkerStart {
        parent: String,
        id: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Tear down a worker perch (T6).
    WorkerStop {
        id: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Drain a worker perch's messages (T6).
    WorkerPoll {
        id: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Rebind the perch to a new session_id, preserving identity (T7).
    Boundary {
        #[arg(value_parser = ["clear", "compact"])]
        mode: String,
        id: String,
        /// The new session id to rebind the perch to.
        #[arg(long = "to-session-id")]
        to_session: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Soft teardown (spool/history preserved); `--erase` hard-wipes (T7).
    SessionEnd {
        id: String,
        #[arg(long)]
        erase: bool,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Report presence (T7).
    Presence {
        id: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Report which node (if any) is remote-driving this endpoint (M4-D5c).
    DrivenBy {
        id: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Append to the native history store (Path-B) (T7).
    HistoryLog {
        id: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Emit a Shell sensory payload (stub) (T7).
    Emit {
        id: String,
        #[arg(long)]
        r#type: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
    /// Print the adapter's declared capability (`hostable_types`) (T7).
    Capability,
    /// Graceful live-agent signoff (M2b): run the final echo-commune BEFORE
    /// teardown (3.3), then soft-stop. The `spt shutdown` lifecycle path.
    Shutdown {
        id: String,
        #[command(flatten)]
        auth: AuthFlags,
    },
}


    /// Consume a seed and hold the perch + relay loop (interim long-runner, T4).
    Listen {
        id: String,
        /// Override the parent-pid anchor (defaults to the self-discovered PPID).
        #[arg(long)]
        parent_pid: Option<u32>,
        /// Drain backlog + one receive cycle, then exit (testability).
        #[arg(long)]
        once: bool,
    },
    /// Post-spawn bind of a session to its perch (T4).
    Bind {
        id: String,
        /// The session id discovered post-spawn, written into the perch record.
        #[arg(long = "set-session-id")]
        bind_session: Option<String>,
        #[command(flatten)]
        auth: AuthFlags,
    },

    /// Consume a seed and hold the perch + relay loop (interim long-runner, T4).
    Listen {
        id: String,
        /// Override the parent-pid anchor (defaults to the self-discovered PPID).
        #[arg(long)]
        parent_pid: Option<u32>,
        /// Drain backlog + one receive cycle, then exit (testability).
        #[arg(long)]
        once: bool,
        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
        /// multi-subnet node — home is assigned at creation, no guessing).
        #[arg(long)]
        subnet: Option<String>,
    },
    /// Post-spawn bind of a session to its perch (T4).
    Bind {
        id: String,
        /// The session id discovered post-spawn, written into the perch record.
        #[arg(long = "set-session-id")]
        bind_session: Option<String>,
        /// Home subnet for a NEW endpoint (REQ-INST-15; see `listen`).
        #[arg(long)]
        subnet: Option<String>,
        #[command(flatten)]
        auth: AuthFlags,
    },


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)

    /// Consume a seed and hold the perch + relay loop (interim long-runner, T4).
    Listen {
        id: String,
        /// Override the parent-pid anchor (defaults to the self-discovered PPID).
        #[arg(long)]
        parent_pid: Option<u32>,
        /// Drain backlog + one receive cycle, then exit (testability).
        #[arg(long)]
        once: bool,
    },
    /// Post-spawn bind of a session to its perch (T4).
    Bind {
        id: String,
        /// The session id discovered post-spawn, written into the perch record.
        #[arg(long = "set-session-id")]
        bind_session: Option<String>,
        #[command(flatten)]
        auth: AuthFlags,
    },

    /// Consume a seed and hold the perch + relay loop (interim long-runner, T4).
    Listen {
        id: String,
        /// Override the parent-pid anchor (defaults to the self-discovered PPID).
        #[arg(long)]
        parent_pid: Option<u32>,
        /// Drain backlog + one receive cycle, then exit (testability).
        #[arg(long)]
        once: bool,
        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
        /// multi-subnet node — home is assigned at creation, no guessing).
        #[arg(long)]
        subnet: Option<String>,
    },
    /// Post-spawn bind of a session to its perch (T4).
    Bind {
        id: String,
        /// The session id discovered post-spawn, written into the perch record.
        #[arg(long = "set-session-id")]
        bind_session: Option<String>,
        /// Home subnet for a NEW endpoint (REQ-INST-15; see `listen`).
        #[arg(long)]
        subnet: Option<String>,
        #[command(flatten)]
        auth: AuthFlags,
    },

+        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a

+        /// multi-subnet node — home is assigned at creation, no guessing).

+        /// Home subnet for a NEW endpoint (REQ-INST-15; see `listen`).


        ApiCmd::Listen { id, parent_pid, once } => {
            startup::cmd_listen(&id, parent_pid, once, ctx.manifest.as_ref())
        }
        ApiCmd::Bind { id, bind_session, auth } => {
            startup::cmd_bind(&id, bind_session, auth.session_id)
        }

        ApiCmd::Listen { id, parent_pid, once, subnet } => {
            startup::cmd_listen(&id, parent_pid, once, ctx.manifest.as_ref(), subnet.as_deref())
        }
        ApiCmd::Bind { id, bind_session, subnet, auth } => {
            // The bind path's adapter source is the api group's own
            // --adapter (no seed on this path) — the adapter-at-creation rule.
            let adapter = Some(ctx.adapter.as_str()).filter(|a| !a.is_empty());
            startup::cmd_bind(&id, bind_session, auth.session_id, adapter, subnet.as_deref())
        }


        ApiCmd::Listen { id, parent_pid, once } => {
            startup::cmd_listen(&id, parent_pid, once, ctx.manifest.as_ref())
        }
        ApiCmd::Bind { id, bind_session, auth } => {
            startup::cmd_bind(&id, bind_session, auth.session_id)
        }

        ApiCmd::Listen { id, parent_pid, once, subnet } => {
            startup::cmd_listen(&id, parent_pid, once, ctx.manifest.as_ref(), subnet.as_deref())
        }
        ApiCmd::Bind { id, bind_session, subnet, auth } => {
            // The bind path's adapter source is the api group's own
            // --adapter (no seed on this path) — the adapter-at-creation rule.
            let adapter = Some(ctx.adapter.as_str()).filter(|a| !a.is_empty());
            startup::cmd_bind(&id, bind_session, auth.session_id, adapter, subnet.as_deref())
        }

// Harness-hosted startup (T4). Seed establishes; listen consumes+binds;

// bind is the post-spawn establishing call (intrinsic auth).

ApiCmd::Seed { pid, session_id } => startup::cmd_seed(&ctx, pid, session_id),

-        ApiCmd::Listen { id, parent_pid, once } => {

-            startup::cmd_listen(&id, parent_pid, once, ctx.manifest.as_ref())

+        ApiCmd::Listen { id, parent_pid, once, subnet } => {

+            startup::cmd_listen(&id, parent_pid, once, ctx.manifest.as_ref(), subnet.as_deref())

-        ApiCmd::Bind { id, bind_session, auth } => {

-            startup::cmd_bind(&id, bind_session, auth.session_id)

+        ApiCmd::Bind { id, bind_session, subnet, auth } => {

+            // The bind path's adapter source is the api group's own

+            // --adapter (no seed on this path) — the adapter-at-creation rule.

+            let adapter = Some(ctx.adapter.as_str()).filter(|a| !a.is_empty());

+            startup::cmd_bind(&id, bind_session, auth.session_id, adapter, subnet.as_deref())

// Delivery + activity (T5), gated by auth.

ApiCmd::State { state, id, no_gate, auth } => {


cli.rs Ready/Poll flags + create-new sweep, registryhost sweep:


    /// Become reachable: register the perch and listen for messages (blocks).
    Ready {
        /// This agent's perch id.
        id: String,
    },
    /// Receive messages: drain backlog then listen, printing each to stdout.
    Poll {
        /// This agent's perch id.
        id: String,
        /// Run a single drain+receive cycle, then exit (legacy one-shot).
        #[arg(long)]
        once: bool,
    },

    /// Become reachable: register the perch and listen for messages (blocks).
    Ready {
        /// This agent's perch id.
        id: String,
        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
        /// multi-subnet node — home is assigned at creation, no guessing).
        #[arg(long)]
        subnet: Option<String>,
    },
    /// Receive messages: drain backlog then listen, printing each to stdout.
    Poll {
        /// This agent's perch id.
        id: String,
        /// Run a single drain+receive cycle, then exit (legacy one-shot).
        #[arg(long)]
        once: bool,
        /// Home subnet for a NEW endpoint (REQ-INST-15; see `ready`).
        #[arg(long)]
        subnet: Option<String>,
    },


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)

    /// Become reachable: register the perch and listen for messages (blocks).
    Ready {
        /// This agent's perch id.
        id: String,
    },
    /// Receive messages: drain backlog then listen, printing each to stdout.
    Poll {
        /// This agent's perch id.
        id: String,
        /// Run a single drain+receive cycle, then exit (legacy one-shot).
        #[arg(long)]
        once: bool,
    },

    /// Become reachable: register the perch and listen for messages (blocks).
    Ready {
        /// This agent's perch id.
        id: String,
        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
        /// multi-subnet node — home is assigned at creation, no guessing).
        #[arg(long)]
        subnet: Option<String>,
    },
    /// Receive messages: drain backlog then listen, printing each to stdout.
    Poll {
        /// This agent's perch id.
        id: String,
        /// Run a single drain+receive cycle, then exit (legacy one-shot).
        #[arg(long)]
        once: bool,
        /// Home subnet for a NEW endpoint (REQ-INST-15; see `ready`).
        #[arg(long)]
        subnet: Option<String>,
    },

+        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a

+        /// multi-subnet node — home is assigned at creation, no guessing).

+        /// Home subnet for a NEW endpoint (REQ-INST-15; see `ready`).

/// List perches under the owlery (id, state, address, ready, alive).


        Cmd::Ready { id } => run_listen(&id, false),
        Cmd::Poll { id, once } => run_listen(&id, once),

        Cmd::Ready { id, subnet } => run_listen(&id, false, subnet.as_deref()),
        Cmd::Poll { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),


        Cmd::Ready { id } => run_listen(&id, false),
        Cmd::Poll { id, once } => run_listen(&id, once),

        Cmd::Ready { id, subnet } => run_listen(&id, false, subnet.as_deref()),
        Cmd::Poll { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),

Cmd::Send { target, from, reply_to, deferred } => cmd_send(target, from, reply_to, deferred),

Cmd::Ring { target, from, timeout } => cmd_ring(target, from, timeout),

-        Cmd::Ready { id } => run_listen(&id, false),

-        Cmd::Poll { id, once } => run_listen(&id, once),

+        Cmd::Ready { id, subnet } => run_listen(&id, false, subnet.as_deref()),

+        Cmd::Poll { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),


fn run_listen(id: &str, once: bool) -> i32 {
    let (agent, backlog) = match ReadyAgent::start(id) {

fn run_listen(id: &str, once: bool, subnet: Option<&str>) -> i32 {
    let (agent, backlog) = match ReadyAgent::start_homed(id, subnet) {


fn run_listen(id: &str, once: bool) -> i32 {
    let (agent, backlog) = match ReadyAgent::start(id) {

fn run_listen(id: &str, once: bool, subnet: Option<&str>) -> i32 {
    let (agent, backlog) = match ReadyAgent::start_homed(id, subnet) {

/// Interim M1 model: the perch is reachable only while this process runs. M3's

/// spt-daemon decouples registration from the live process.

-fn run_listen(id: &str, once: bool) -> i32 {

-    let (agent, backlog) = match ReadyAgent::start(id) {

+fn run_listen(id: &str, once: bool, subnet: Option<&str>) -> i32 {

+    let (agent, backlog) = match ReadyAgent::start_homed(id, subnet) {


Create-new adoption sweep + registryhost sweep:


    if result.save {
        if let Err(e) = store.save() {
            eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
            return 1;
        }
    }

    if result.save {
        if let Err(e) = store.save() {
            eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
            return 1;
        }
        // First-join home adoption (REQ-INST-15): a fresh subnet on a
        // previously-unpaired node homes every local-only Self perch.
        let mut vis = spt_store::visibility::VisibilityStore::load();
        let adopted =
            spt_store::home::adopt_for_unset(&perch::owlery_dir(), &store, &mut vis);
        if !adopted.is_empty() {
            let _ = vis.save();
            eprintln!("HOME_ADOPTED:{}", adopted.join(","));
        }
    }


    if result.save {
        if let Err(e) = store.save() {
            eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
            return 1;
        }
    }

    if result.save {
        if let Err(e) = store.save() {
            eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
            return 1;
        }
        // First-join home adoption (REQ-INST-15): a fresh subnet on a
        // previously-unpaired node homes every local-only Self perch.
        let mut vis = spt_store::visibility::VisibilityStore::load();
        let adopted =
            spt_store::home::adopt_for_unset(&perch::owlery_dir(), &store, &mut vis);
        if !adopted.is_empty() {
            let _ = vis.save();
            eprintln!("HOME_ADOPTED:{}", adopted.join(","));
        }
    }

+        // First-join home adoption (REQ-INST-15): a fresh subnet on a

+        // previously-unpaired node homes every local-only Self perch.

+        let mut vis = spt_store::visibility::VisibilityStore::load();

+            spt_store::home::adopt_for_unset(&perch::owlery_dir(), &store, &mut vis);

+            eprintln!("HOME_ADOPTED:{}", adopted.join(","));


    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
        let subnets = SubnetStore::load();
        let vis = VisibilityStore::load();

    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
        let subnets = SubnetStore::load();
        let mut vis = VisibilityStore::load();
        // First-join home adoption (D9-5, REQ-INST-15): within one
        // advertisement cadence of the node's first join (a pair lands the
        // subnet record; no pairing-protocol threading needed), unhomed Self
        // perches adopt the sole subnet + seed their sync scope. Strictly
        // adopt-if-unset; a multi-subnet store adopts nothing.
        // [impl->REQ-INST-15]
        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {
            let _ = vis.save();
        }
        let vis = vis;


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

    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
        let subnets = SubnetStore::load();
        let vis = VisibilityStore::load();

    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
        let subnets = SubnetStore::load();
        let mut vis = VisibilityStore::load();
        // First-join home adoption (D9-5, REQ-INST-15): within one
        // advertisement cadence of the node's first join (a pair lands the
        // subnet record; no pairing-protocol threading needed), unhomed Self
        // perches adopt the sole subnet + seed their sync scope. Strictly
        // adopt-if-unset; a multi-subnet store adopts nothing.
        // [impl->REQ-INST-15]
        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {
            let _ = vis.save();
        }
        let vis = vis;

-        let vis = VisibilityStore::load();

+        let mut vis = VisibilityStore::load();

+        // First-join home adoption (D9-5, REQ-INST-15): within one

+        // advertisement cadence of the node's first join (a pair lands the

+        // subnet record; no pairing-protocol threading needed), unhomed Self

+        // perches adopt the sole subnet + seed their sync scope. Strictly

+        // adopt-if-unset; a multi-subnet store adopts nothing.

+        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {

// The node-level resource-blurb seed (D9-4): an endpoint that has not

// authored its own blurb advertises this default (config seeds, the

// agent refines — CONTEXT §resource advertisement).


Build + fix fallout:


PowerShell

cargo build --workspace --all-targets 2>&1 | Select-String -Pattern "^error" -Context 0,6 | Select-Object -First 12

Build, surface errors


(PowerShell completed with no output)


cargo test --workspace *> $null; "TESTS=$LASTEXITCODE"; cargo clippy --workspace --all-targets -- -D warnings *> $null; "CLIPPY=$LASTEXITCODE"; traceable-reqs check *> $null; "TRACE=$LASTEXITCODE"

Full gates for D9-5-1


TESTS=0
CLIPPY=0
TRACE=0


All green. Commit + push D9-5-1:


git add -A; git commit -m @'
feat(store,spt,daemon): M4-D9-5-1 immutable home subnet at creation

The home-subnet half of REQ-INST-15 (ADR-0010; activation lands with the
fork half):

- spt-store::home — assign_home (the CONTEXT matrix: sole subnet auto,
  several refuse-and-qualify, unpaired local-only; an explicit pick must
  be a member) + stamp_creation_fields, the ONE shared creation seam: a
  NEW perch assigns its immutable home hard and seeds
  sync_subnets = [home] (the visibility store's fail-closed
  "unconfigured syncs nowhere" finally gets its creation-flow seed); a
  REVIVE carries home, adapter, resource blurb, and the auto-suspend
  override forward — a re-bind rewrites info.json and must not wipe
  creation/user state (an explicit adapter wins: resume-under-new is the
  sanctioned adapter change). NO setter exists - immutability by
  construction.
- info.json grows home_subnet + adapter (additive serde).
- Creation surfaces thread the seam: ReadyAgent::start_homed
  (spt ready/poll --subnet), api listen/bind --subnet (the seed carries
  the creating adapter on the listen path; the api group's --adapter on
  the bind path). BindError grows Home (refuse-and-qualify is loud).
- First-join adoption (adopt_for_unset): when the node holds exactly ONE
  subnet, unhomed Self perches adopt it + seed scopes — immediately on
  the subnet-creation CLI path, and within one advertisement cadence of
  a pair join via the daemon registry scan (no pairing-protocol
  threading). Never re-homes, never guesses on a multi-subnet store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@; git push origin dev-freeform

Commit and push D9-5-1


warning: in the working copy of 'crates/spt-daemon/src/registryhost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-msg/src/ready.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/api/mod.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/api/startup.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/home.rs', LF will be replaced by CRLF the next time Git touches it
[dev-freeform e69f59a] feat(store,spt,daemon): M4-D9-5-1 immutable home subnet at creation
 8 files changed, 560 insertions(+), 27 deletions(-)
 create mode 100644 crates/spt-store/src/home.rs
To https://github.com/SaberMage/spt-core.git
   0ea318b..e69f59a  dev-freeform -> dev-freeform


Bash

until gh run list --branch dev-freeform --limit 1 --json status,conclusion,headSha --jq '.[0]' | grep -q '"status":"completed"'; do sleep 30; done; gh run list --branch dev-freeform --limit 1 --json status,conclusion,headSha --jq '.[0] | "\(.headSha[0:7]) \(.status) \(.conclusion)"'

Wait for D9-5-1 CI run


Command running in background with ID: bwlsid7km. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bwlsid7km.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bwlsid7km</task-id>
<tool-use-id>toolu_01KrxhUNqFZoYDiQd9oc5qCn</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bwlsid7km.output</output-file>
<status>completed</status>
<summary>Background command "Wait for D9-5-1 CI run" completed (exit code 0)</summary>
</task-notification>


D9-5-1 pushed, watcher armed. Build D9-5-2 (fork) — contextstore methods first:


Edit

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

impl ContextStore {
    /// The context arm of `spt fork <src> <new>` (D9-5, REQ-INST-15 —
    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s live
    /// + project tiers, after which the two diverge — the copy lands as
    /// fresh seed commits on `a-<new>` / under `<new>/` in each project
    /// worktree (**no shared history, no ongoing sync**: a fork is a new
    /// identity, not an instance). Conflict artifacts are deliberately not
    /// copied — the fork starts clean. Refuses when `a-<new>` already exists
    /// (never merge two minds — the rename rule); a source with no tracked
    /// mind forks as an empty (perch-only) endpoint, `Ok`.
    // [impl->REQ-INST-15]
    pub fn fork_endpoint(&self, src: &str, new: &str) -> std::io::Result<()> {
        Self::validated(src)?;
        Self::validated(new)?;
        if self.store.has_branch(&agent_branch(new))? {
            return Err(std::io::Error::new(
                std::io::ErrorKind::AlreadyExists,
                format!("context branch already exists: {}", agent_branch(new)),
            ));
        }

        // Live tier: copy the tip content into a fresh, parentless a-<new>.
        if self.store.has_branch(&agent_branch(src))? {
            if let Some(bytes) =
                self.store.read_at_tip(&agent_branch(src), LIVE_CONTEXT_FILE)?
            {
                let dest = self.live_context_path(new)?;
                crate::atomic::atomic_write_string(&dest, &String::from_utf8_lossy(&bytes))?;
                self.commit_live(new, &format!("fork of {src} (one-time copy)"))?;
            }
        }

        // Project tiers: copy src's slice in every project worktree holding one.
        let projects = self.root.join("projects");
        if let Ok(entries) = std::fs::read_dir(&projects) {
            for entry in entries.flatten() {
                let wt = entry.path();
                let src_file = wt.join(src).join(PROJECT_CONTEXT_FILE);
                if !wt.join(".git").exists() || !src_file.exists() {
                    continue;
                }
                let Some(project_id) =
                    entry.file_name().to_str().map(str::to_string)
                else {
                    continue;
                };
                let bytes = std::fs::read(&src_file)?;
                let dest = self.project_context_path(&project_id, new)?;
                crate::atomic::atomic_write_string(&dest, &String::from_utf8_lossy(&bytes))?;
                self.commit_project(&project_id, &format!("fork of {src} -> {new}"))?;
            }
        }
        Ok(())
    }

    /// Remove an endpoint's tracked mind — the `--delete-source` arm of
    /// `spt fork` (and nothing else: deletion is explicit, never implied).
    /// Removes the `a-<id>` worktree + branch and `<id>/` inside every
    /// project worktree (one removal commit per touched project). Absent
    /// pieces are no-ops — removing exactly what exists, idempotently.
    // [impl->REQ-INST-15]
    pub fn remove_endpoint(&self, id: &str) -> std::io::Result<()> {
        Self::validated(id)?;
        if self.store.has_branch(&agent_branch(id))? {
            let wt = self.root.join("agents").join(id);
            if wt.join(".git").exists() {
                crate::gitrun::run_git_ok(
                    &[
                        "-C",
                        &self.root.join(".seed.git").to_string_lossy(),
                        "worktree",
                        "remove",
                        "--force",
                        &wt.to_string_lossy(),
                    ],
                    None,
                    None,
                )?;
            }
            crate::gitrun::run_git_ok(
                &[
                    "-C",
                    &self.root.join(".seed.git").to_string_lossy(),
                    "branch",
                    "-D",
                    &agent_branch(id),
                ],
                None,
                None,
            )?;
        }
        let projects = self.root.join("projects");
        if let Ok(entries) = std::fs::read_dir(&projects) {
            for entry in entries.flatten() {
                let wt = entry.path();
                if !wt.join(".git").exists() || !wt.join(id).exists() {
                    continue;
                }
                crate::gitrun::run_git_ok(&["rm", "-r", id], Some(&wt), None)?;
                self.store.commit_in_worktree(&wt, &[], &format!("remove {id} (fork source deleted)"))?;
            }
        }
        Ok(())
    }
}

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