fn cmd_resources(action: ResourcesCmd) -> i32 {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    /// Resolve the operated endpoint: explicit `--id`, else session detect.
    fn target(id: Option<String>) -> Result<String, i32> {
        match id.or_else(roster::detect_self_id) {
            Some(id) => Ok(id),
            None => {
                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");
                Err(1)
            }
        }
    }

    match action {
        ResourcesCmd::Set { text, id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            let blurb = (!text.trim().is_empty()).then_some(text.trim());
            match spt_store::info::set_resources(&perch_path, blurb) {
                Ok(()) => {
                    match blurb {
                        Some(b) => eprintln!("RESOURCES_SET:{id}: {b}"),
                        None => eprintln!("RESOURCES_CLEARED:{id} (node seed advertises, if any)"),
                    }
                    0
                }
                Err(e) => {
                    eprintln!("RESOURCES_FAIL:{id}: {e}");
                    1
                }
            }
        }
        ResourcesCmd::Show { id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            match spt_store::info::read_info(&perch_path) {
                Some(rec) => {
                    match rec.resources {
                        Some(b) => println!("{b}"),
                        None => println!("(no authored blurb — the node seed advertises, if any)"),
                    }
                    0
                }
                None => {
                    eprintln!("NO_ENDPOINT:{id}");
                    1
                }
            }
        }
        ResourcesCmd::List { subnet } => {
            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
            let subnets = SubnetStore::load();
            let vis = VisibilityStore::load();
            let names: Vec<String> = match subnet {
                Some(one) => vec![one],
                None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),
            };
            let mut shown = 0usize;
            for name in names {
                let Some(reg) = regs.get(&name) else { continue };
                let hide_new = subnets
                    .find(&name)
                    .map(|s| s.hide_new_endpoints)
                    .unwrap_or(false);
                // The same exclusion the resolver enforces — the projection
                // can never list what resolution would refuse to route to.
                let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));
                for row in rows {
                    println!(
                        "{}\t{}\t{}\t{:?}\t{}",
                        name,
                        row.endpoint_id,
                        row.node,
                        row.status,
                        row.resources.as_deref().unwrap_or("-")
                    );
                    shown += 1;
                }
            }
            eprintln!("RESOURCES:{shown}");
            0
        }
    }
}

fn cmd_description(action: DescriptionCmd) -> i32 {
    /// Resolve the operated endpoint: explicit `--id`, else session detect.
    fn target(id: Option<String>) -> Result<String, i32> {
        match id.or_else(roster::detect_self_id) {
            Some(id) => Ok(id),
            None => {
                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");
                Err(1)
            }
        }
    }

    match action {
        DescriptionCmd::Set { text, id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            let blurb = (!text.trim().is_empty()).then_some(text.trim());
            match spt_store::info::set_resources(&perch_path, blurb) {
                Ok(()) => {
                    match blurb {
                        Some(b) => eprintln!("DESCRIPTION_SET:{id}: {b}"),
                        None => {
                            eprintln!("DESCRIPTION_CLEARED:{id} (node seed advertises, if any)")
                        }
                    }
                    0
                }
                Err(e) => {
                    eprintln!("DESCRIPTION_FAIL:{id}: {e}");
                    1
                }
            }
        }
        DescriptionCmd::Show { id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            match spt_store::info::read_info(&perch_path) {
                Some(rec) => {
                    match rec.resources {
                        Some(b) => println!("{b}"),
                        None => println!("(no authored blurb — the node seed advertises, if any)"),
                    }
                    0
                }
                None => {
                    eprintln!("NO_ENDPOINT:{id}");
                    1
                }
            }
        }
    }
}

/// `spt endpoint list` — the merged endpoint listing (M8 decision 2,
/// REQ-CLI-1): every member subnet's VISIBLE endpoints grouped by subnet
/// (the same exclusion closure the resolver enforces — the list can never
/// show what resolution would refuse to route to), with this session's own
/// endpoint pinned distinctly at the top. `--detail` adds the description
/// blurbs (the ex-`resources list` yellow-pages projection, decision 25c);
/// `--local` renders this node's perch roster instead; `--subnet` filters.
// [impl->REQ-CLI-1]
fn cmd_endpoint_list(local: bool, subnet: Option<String>, detail: bool) -> i32 {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    if local {
        return cmd_list_local();
    }
    let subnets = SubnetStore::load();
    if let Some(n) = &subnet {
        if subnets.find(n).is_none() {
            eprintln!("NO_SUCH_SUBNET:{n}");
            return 2;
        }
    }

    // SELF pin (decision 2): the session's own endpoint renders first and
    // distinctly, whatever subnet groups follow.
    if let Some(self_id) = roster::detect_self_id() {
        let state = roster::enumerate()
            .into_iter()
            .find(|p| p.id == self_id)
            .map(|p| format!("{}  ready={} alive={}", p.state, p.ready, p.alive))
            .unwrap_or_else(|| "(no local perch)".to_string());
        println!("SELF: {self_id}  {state}");
    }

    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let names: Vec<String> = match subnet {
        Some(one) => vec![one],
        None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),
    };
    if names.is_empty() {
        eprintln!("(no subnets — `spt endpoint list --local` lists this node's perches)");
        return 0;
    }
    let mut shown = 0usize;
    for name in names {
        println!("SUBNET {name}");
        let Some(reg) = regs.get(&name) else {
            println!("  (no gossip yet)");
            continue;
        };
        let hide_new = subnets
            .find(&name)
            .map(|s| s.hide_new_endpoints)
            .unwrap_or(false);
        let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));
        if rows.is_empty() {
            println!("  (no endpoints)");
            continue;
        }
        for row in rows {
            let node = node_display(&row.node, None);
            if detail {
                println!(
                    "  {}\t{}\t{:?}\t{}",
                    row.endpoint_id,
                    node,
                    row.status,
                    row.resources.as_deref().unwrap_or("-")
                );
            } else {
                println!("  {}\t{}\t{:?}", row.endpoint_id, node, row.status);
            }
            shown += 1;
        }
    }
    eprintln!("ENDPOINTS:{shown}");
    0
}


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)

fn cmd_resources(action: ResourcesCmd) -> i32 {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    /// Resolve the operated endpoint: explicit `--id`, else session detect.
    fn target(id: Option<String>) -> Result<String, i32> {
        match id.or_else(roster::detect_self_id) {
            Some(id) => Ok(id),
            None => {
                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");
                Err(1)
            }
        }
    }

    match action {
        ResourcesCmd::Set { text, id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            let blurb = (!text.trim().is_empty()).then_some(text.trim());
            match spt_store::info::set_resources(&perch_path, blurb) {
                Ok(()) => {
                    match blurb {
                        Some(b) => eprintln!("RESOURCES_SET:{id}: {b}"),
                        None => eprintln!("RESOURCES_CLEARED:{id} (node seed advertises, if any)"),
                    }
                    0
                }
                Err(e) => {
                    eprintln!("RESOURCES_FAIL:{id}: {e}");
                    1
                }
            }
        }
        ResourcesCmd::Show { id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            match spt_store::info::read_info(&perch_path) {
                Some(rec) => {
                    match rec.resources {
                        Some(b) => println!("{b}"),
                        None => println!("(no authored blurb — the node seed advertises, if any)"),
                    }
                    0
                }
                None => {
                    eprintln!("NO_ENDPOINT:{id}");
                    1
                }
            }
        }
        ResourcesCmd::List { subnet } => {
            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
            let subnets = SubnetStore::load();
            let vis = VisibilityStore::load();
            let names: Vec<String> = match subnet {
                Some(one) => vec![one],
                None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),
            };
            let mut shown = 0usize;
            for name in names {
                let Some(reg) = regs.get(&name) else { continue };
                let hide_new = subnets
                    .find(&name)
                    .map(|s| s.hide_new_endpoints)
                    .unwrap_or(false);
                // The same exclusion the resolver enforces — the projection
                // can never list what resolution would refuse to route to.
                let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));
                for row in rows {
                    println!(
                        "{}\t{}\t{}\t{:?}\t{}",
                        name,
                        row.endpoint_id,
                        row.node,
                        row.status,
                        row.resources.as_deref().unwrap_or("-")
                    );
                    shown += 1;
                }
            }
            eprintln!("RESOURCES:{shown}");
            0
        }
    }
}

fn cmd_description(action: DescriptionCmd) -> i32 {
    /// Resolve the operated endpoint: explicit `--id`, else session detect.
    fn target(id: Option<String>) -> Result<String, i32> {
        match id.or_else(roster::detect_self_id) {
            Some(id) => Ok(id),
            None => {
                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");
                Err(1)
            }
        }
    }

    match action {
        DescriptionCmd::Set { text, id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            let blurb = (!text.trim().is_empty()).then_some(text.trim());
            match spt_store::info::set_resources(&perch_path, blurb) {
                Ok(()) => {
                    match blurb {
                        Some(b) => eprintln!("DESCRIPTION_SET:{id}: {b}"),
                        None => {
                            eprintln!("DESCRIPTION_CLEARED:{id} (node seed advertises, if any)")
                        }
                    }
                    0
                }
                Err(e) => {
                    eprintln!("DESCRIPTION_FAIL:{id}: {e}");
                    1
                }
            }
        }
        DescriptionCmd::Show { id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            match spt_store::info::read_info(&perch_path) {
                Some(rec) => {
                    match rec.resources {
                        Some(b) => println!("{b}"),
                        None => println!("(no authored blurb — the node seed advertises, if any)"),
                    }
                    0
                }
                None => {
                    eprintln!("NO_ENDPOINT:{id}");
                    1
                }
            }
        }
    }
}

/// `spt endpoint list` — the merged endpoint listing (M8 decision 2,
/// REQ-CLI-1): every member subnet's VISIBLE endpoints grouped by subnet
/// (the same exclusion closure the resolver enforces — the list can never
/// show what resolution would refuse to route to), with this session's own
/// endpoint pinned distinctly at the top. `--detail` adds the description
/// blurbs (the ex-`resources list` yellow-pages projection, decision 25c);
/// `--local` renders this node's perch roster instead; `--subnet` filters.
// [impl->REQ-CLI-1]
fn cmd_endpoint_list(local: bool, subnet: Option<String>, detail: bool) -> i32 {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    if local {
        return cmd_list_local();
    }
    let subnets = SubnetStore::load();
    if let Some(n) = &subnet {
        if subnets.find(n).is_none() {
            eprintln!("NO_SUCH_SUBNET:{n}");
            return 2;
        }
    }

    // SELF pin (decision 2): the session's own endpoint renders first and
    // distinctly, whatever subnet groups follow.
    if let Some(self_id) = roster::detect_self_id() {
        let state = roster::enumerate()
            .into_iter()
            .find(|p| p.id == self_id)
            .map(|p| format!("{}  ready={} alive={}", p.state, p.ready, p.alive))
            .unwrap_or_else(|| "(no local perch)".to_string());
        println!("SELF: {self_id}  {state}");
    }

    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let names: Vec<String> = match subnet {
        Some(one) => vec![one],
        None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),
    };
    if names.is_empty() {
        eprintln!("(no subnets — `spt endpoint list --local` lists this node's perches)");
        return 0;
    }
    let mut shown = 0usize;
    for name in names {
        println!("SUBNET {name}");
        let Some(reg) = regs.get(&name) else {
            println!("  (no gossip yet)");
            continue;
        };
        let hide_new = subnets
            .find(&name)
            .map(|s| s.hide_new_endpoints)
            .unwrap_or(false);
        let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));
        if rows.is_empty() {
            println!("  (no endpoints)");
            continue;
        }
        for row in rows {
            let node = node_display(&row.node, None);
            if detail {
                println!(
                    "  {}\t{}\t{:?}\t{}",
                    row.endpoint_id,
                    node,
                    row.status,
                    row.resources.as_deref().unwrap_or("-")
                );
            } else {
                println!("  {}\t{}\t{:?}", row.endpoint_id, node, row.status);
            }
            shown += 1;
        }
    }
    eprintln!("ENDPOINTS:{shown}");
    0
}

/// visibility-filtered exactly like resolution — a hidden endpoint never

/// appears (ADR-0006 §6, exclusion by construction).

-fn cmd_resources(action: ResourcesCmd) -> i32 {

-    use spt_net::net::registry::resource_projection;

-    use spt_store::visibility::VisibilityStore;

+fn cmd_description(action: DescriptionCmd) -> i32 {

-        ResourcesCmd::Set { text, id } => {

+        DescriptionCmd::Set { text, id } => {

-                        Some(b) => eprintln!("RESOURCES_SET:{id}: {b}"),

-                        None => eprintln!("RESOURCES_CLEARED:{id} (node seed advertises, if any)"),

+                        Some(b) => eprintln!("DESCRIPTION_SET:{id}: {b}"),

+                            eprintln!("DESCRIPTION_CLEARED:{id} (node seed advertises, if any)")

-                    eprintln!("RESOURCES_FAIL:{id}: {e}");

+                    eprintln!("DESCRIPTION_FAIL:{id}: {e}");

+        DescriptionCmd::Show { id } => {

-        ResourcesCmd::List { subnet } => {

-            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));

-            let subnets = SubnetStore::load();

-            let vis = VisibilityStore::load();

-            let names: Vec<String> = match subnet {

-                Some(one) => vec![one],

-                None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),

-                let Some(reg) = regs.get(&name) else { continue };

-                    .map(|s| s.hide_new_endpoints)

-                // The same exclusion the resolver enforces — the projection

-                // can never list what resolution would refuse to route to.

-                let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));

-                        row.endpoint_id,

-                        row.resources.as_deref().unwrap_or("-")

+/// `spt endpoint list` — the merged endpoint listing (M8 decision 2,

+/// REQ-CLI-1): every member subnet's VISIBLE endpoints grouped by subnet

+/// (the same exclusion closure the resolver enforces — the list can never

+/// show what resolution would refuse to route to), with this session's own

+/// endpoint pinned distinctly at the top. `--detail` adds the description

+/// blurbs (the ex-`resources list` yellow-pages projection, decision 25c);

+/// `--local` renders this node's perch roster instead; `--subnet` filters.

+fn cmd_endpoint_list(local: bool, subnet: Option<String>, detail: bool) -> i32 {

+    use spt_net::net::registry::resource_projection;

+    use spt_store::visibility::VisibilityStore;

+            eprintln!("NO_SUCH_SUBNET:{n}");

+    // SELF pin (decision 2): the session's own endpoint renders first and

+    // distinctly, whatever subnet groups follow.

+    if let Some(self_id) = roster::detect_self_id() {

+        let state = roster::enumerate()

+            .map(|p| format!("{}  ready={} alive={}", p.state, p.ready, p.alive))

+            .unwrap_or_else(|| "(no local perch)".to_string());

+        println!("SELF: {self_id}  {state}");

+    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));

+    let names: Vec<String> = match subnet {

+        None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),

+        eprintln!("(no subnets — `spt endpoint list --local` lists this node's perches)");

+        let Some(reg) = regs.get(&name) else {

+            println!("  (no gossip yet)");

+            .map(|s| s.hide_new_endpoints)

+        let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));

+            println!("  (no endpoints)");

+            let node = node_display(&row.node, None);

+                    row.resources.as_deref().unwrap_or("-")

+                println!("  {}\t{}\t{:?}", row.endpoint_id, node, row.status);

-            eprintln!("RESOURCES:{shown}");

/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges


resources --> new flag under `spt endpoint list`: `--detail` --- change the `spt subnet attach/detach --auto` flag to `--save`. clearer semantics.


Update CLI flags for endpoint and subnet commands


85:    /// the "subnet resource registry" is a [projection](resource_projection)
86-    \ of these rows. Additive serde-default: absent = no blurb, and a
87-    /// pre-D9-4 record parses clean.
88-    #[serde(default, skip_serializing_if = "Option::is_none")]
89-    pub resources: Option<String>,
90-    \ The presence datum (M5-D6a, REQ-PRES-1 — CONTEXT §presence): this
91-    /// instance's `last_active_ms` recency stamp as of the authoring node's
92-    /// advertisement scan. **Not a separate gossip channel** — a field on the
93-    /// instance row riding the existing epoch lease + `RegistryUpdate`
94-    \ replication, exactly like [`resources`](Self::resources); subnet-wide
95-    /// `(last_active_node, last_active_endpoint, ts)` is a projection of
96-    /// these rows. Wall-clock ms, cross-compared ONLY as a routing heuristic
97-    /// (most-recently-active resolution) — the epoch stays the sole merge-
98-    /// precedence key; clock skew can cost a suboptimal surface target, never
99-    \ a wrong merge. Additive serde-default: a pre-D6 row parses clean.
100-    // [impl->REQ-PRES-1]
101-    #[serde(default, skip_serializing_if = "Option::is_none")]
102-    pub last_active_ms: Option<u64>,
103-    /// The authoring node's subnet-broadcast shell adapters (M5-D8c — CONTEXT
104-    \ §Shell model "Discovery scope": shell adapters on *other* subnet nodes
105-    /// with `broadcast = subnet` surface to agents). **Not a shell-instance
--
228:pub struct ResourceRow {
229-    pub endpoint_id: String,
230-    pub node: String,
231-    pub status: Status,
232-    /// The advertised blurb (`None` = the endpoint advertises no services —
233-    /// the row still lists; absence renders clean).
234-    pub resources: Option<String>,
235-}
236-
237-/// Project one subnet's registry into its resource-registry view, over
238-\ **visible** rows only: `excluded(id)` is the same visibility closure the
239-\ resolver uses ([`SubnetRegistry::resolve_visible`]), so a hidden endpoint
240-/// never appears — discovery leaks nothing a viewer couldn't reach
241-/// (ADR-0006 §6, exclusion by construction). Unroutable (`Offline`) rows are
242-\ skipped too: a dead instance serves nothing, so it is not a resource.
243-// [impl->REQ-INST-14]
244:pub fn resource_projection(
245-    reg: &SubnetRegistry,
246-    excluded: impl Fn(&str) -> bool,
247-) -> Vec<ResourceRow> {
248-    let mut out = Vec::new();
249-    for id in reg.endpoint_ids() {
250-        if excluded(id) {
251-            continue;
252-        }
253-        for instance in reg.instances(id) {
254-            if !instance.status.routable() {
255-                continue;

[Showing results with pagination = limit: 50]


ResourceRow lacks label — bare key-prefix render OK for D1. Now rename `cmd_list`→`cmd_list_local`, rewrite daemon fns.


Edit

fn cmd_list() -> i32 {
    let perches = roster::enumerate();

/// `spt endpoint list --local` — this node's perch roster (the pre-M8 bare
/// `spt list` view, moved under the endpoint noun: M8 decision 2).
// [impl->REQ-CLI-1]
fn cmd_list_local() -> i32 {
    let perches = roster::enumerate();


fn cmd_list() -> i32 {
    let perches = roster::enumerate();

/// `spt endpoint list --local` — this node's perch roster (the pre-M8 bare
/// `spt list` view, moved under the endpoint noun: M8 decision 2).
// [impl->REQ-CLI-1]
fn cmd_list_local() -> i32 {
    let perches = roster::enumerate();

+/// `spt endpoint list --local` — this node's perch roster (the pre-M8 bare

+/// `spt list` view, moved under the endpoint noun: M8 decision 2).


Ratify M8-PLAN.md — fold A–I resolutions in, status DRAFT→RATIFIED

Move resolved decisions A-G + new H/I into ratified table; update D1-D5 deliverable text (NTP, debug-converge, peer-addrs.json, pump liveness, hazard entry); update acceptance; commit.

Mint M8 requirements in traceable-reqs.toml (rule 3)

REQ-CLI-* (endpoint noun, daemon noun, hot-path), REQ-SUBNET-* (attach/detach, prune, re-pair overwrite, status render), REQ-INSTALL-* (Linux elevation leg, firewall, OS-service), NTP/TOTP offset, pump liveness, addr seeding, event advertisement, debug-converge activation (REQ-UPD-6 int), epoch-reset REQ-HAZARD-* (stages=[]). Activate only what D1 starts; rest staged per rule 5.

D1 — endpoint + daemon nouns (breaking CLI reorg)

Namespace skeletons; move fork/suspend/wake/shutdown/rename/stop/digest + access (1:1, decision F) + description under endpoint; merged endpoint list (SELF pinned, subnet-grouped); notify → subnet notify; daemon run|stop|status (status renders pump heartbeat per H); help groups; how-to topics; cli/reference.md regen.

D2 — subnet serve-state, trust lifecycle, status UX

attach/detach machinery + --auto persistence; subnet leave (elevation); subnet prune (elevation, BIGNET 09ef831e fixture); re-pair trust overwrite (machine id per G: OS id hashed, UUID fallback; additive serde field); status render fixes (decisions 11/12). Disband deferred (A).

D3 — install & elevation hardening

Linux: install.sh symlink to /usr/local/bin + first-sudo account election + KH 5.7 verify. Windows: firewall rule (elevated leg) + blocked-inbound self-detection render. OS-service per B: systemd user service + enable-linger; Windows scheduled task at-logon. E2E rungs env-gated.

D4 — convergence polish + pump resilience

peer-addrs.json seeding per E/14 (both cold starts, id-only fallback); event-driven advertisement (decision 15); NTP TOTP-offset per C/C2 (ceremony-time, system-clock fallback); pump supervision per H (panic catch + capped-backoff restart, heartbeat file); xtask debug-converge watcher per D (REQ-UPD-6 int).

D5 — docs + CI + acceptance prep

Docs-site verb sweep; quickstart/how-to deltas; README; llms.txt; reference regen; KNOWN-HAZARDS epoch-reset entry per I; quickstart_e2e + twohost ladder updates; traceable-reqs check + xtask check green.


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

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

/// `spt daemon stop` — graceful daemon shutdown over the seed-control
/// channel (M8 decision 5, REQ-CLI-2): the daemon acks, then exits its
/// foreground loop. Idempotent UX: a daemon that is not running is already
/// stopped (exit 0, said honestly).
// [impl->REQ-CLI-2]
fn cmd_daemon_stop() -> i32 {
    if !spt_daemon::is_running() {
        eprintln!("DAEMON_NOT_RUNNING (already stopped)");
        return 0;
    }
    match spt_daemon::request_stop() {
        Ok(()) => {
            eprintln!("DAEMON_STOPPED");
            0
        }
        Err(e) => {
            eprintln!("DAEMON_STOP_FAIL: {e}");
            1
        }
    }
}

/// `spt daemon [status]` — the node status view (M8 decisions 23 + 25b,
/// REQ-CLI-2): daemon state (running/stopped + the peer-pump heartbeat —
/// a stalled pump renders honestly, never implied-healthy), the member
/// subnets, and this node's local endpoints. Read-only and ungated.
// [impl->REQ-CLI-2]
fn cmd_daemon_status() -> i32 {
    // Daemon state: the socket ping is liveness truth (never the pid file).
    let running = spt_daemon::is_running();
    if running {
        let pid = std::fs::read_to_string(spt_daemon::daemon_pid_path())
            .ok()
            .map(|s| s.trim().to_string());
        match pid {
            Some(p) if !p.is_empty() => println!("daemon: running (pid {p})"),
            _ => println!("daemon: running"),
        }
        // Pump liveness (M8 decision 23 leg 1): the heartbeat file the pump
        // writes each tick (D4). Absent = no heartbeat written yet (an old
        // daemon, or one mid-startup) — said as unknown, never as healthy.
        match spt_daemon::read_pump_heartbeat() {
            Some(at_ms) => {
                let age = now_ms().saturating_sub(at_ms);
                let stale_after = 3 * spt_daemon::DaemonConfig::load()
                    .registry_pump_period
                    .as_millis() as u64;
                if age <= stale_after {
                    println!("peer pump: live (last tick {}s ago)", age / 1000);
                } else {
                    println!(
                        "peer pump: STALLED (last tick {}s ago — advertising/connecting \
                         is not running; restart the daemon)",
                        age / 1000
                    );
                }
            }
            None => println!("peer pump: no heartbeat recorded"),
        }
    } else {
        println!("daemon: not running (`spt daemon run`, or any spt command auto-starts it)");
    }

    // Member subnets (names + counts — the bare-subnet status rows).
    let store = SubnetStore::load();
    if store.subnets.is_empty() {
        println!("subnets: (none — this node is standalone)");
    } else {
        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
        let trust = spt_store::trust::TrustStore::load();
        let own_hex = spt_store::nodeid::load_or_create()
            .map(|i| i.public_key().to_hex())
            .unwrap_or_default();
        println!("subnets:");
        for row in subnet_status_rows(
            &store,
            &regs,
            &trust,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            None,
        ) {
            println!(
                "  {}  nodes={} endpoints={}",
                row.name, row.nodes, row.endpoints
            );
        }
    }

    // Local endpoints (the perch roster).
    let perches = roster::enumerate();
    if perches.is_empty() {
        println!("local endpoints: (none)");
    } else {
        println!("local endpoints:");
        for p in perches {
            println!(
                "  {:<28} {:<14} ready={} alive={}",
                p.id, p.state, p.ready, p.alive
            );
        }
    }
    0
}


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

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

/// `spt daemon stop` — graceful daemon shutdown over the seed-control
/// channel (M8 decision 5, REQ-CLI-2): the daemon acks, then exits its
/// foreground loop. Idempotent UX: a daemon that is not running is already
/// stopped (exit 0, said honestly).
// [impl->REQ-CLI-2]
fn cmd_daemon_stop() -> i32 {
    if !spt_daemon::is_running() {
        eprintln!("DAEMON_NOT_RUNNING (already stopped)");
        return 0;
    }
    match spt_daemon::request_stop() {
        Ok(()) => {
            eprintln!("DAEMON_STOPPED");
            0
        }
        Err(e) => {
            eprintln!("DAEMON_STOP_FAIL: {e}");
            1
        }
    }
}

/// `spt daemon [status]` — the node status view (M8 decisions 23 + 25b,
/// REQ-CLI-2): daemon state (running/stopped + the peer-pump heartbeat —
/// a stalled pump renders honestly, never implied-healthy), the member
/// subnets, and this node's local endpoints. Read-only and ungated.
// [impl->REQ-CLI-2]
fn cmd_daemon_status() -> i32 {
    // Daemon state: the socket ping is liveness truth (never the pid file).
    let running = spt_daemon::is_running();
    if running {
        let pid = std::fs::read_to_string(spt_daemon::daemon_pid_path())
            .ok()
            .map(|s| s.trim().to_string());
        match pid {
            Some(p) if !p.is_empty() => println!("daemon: running (pid {p})"),
            _ => println!("daemon: running"),
        }
        // Pump liveness (M8 decision 23 leg 1): the heartbeat file the pump
        // writes each tick (D4). Absent = no heartbeat written yet (an old
        // daemon, or one mid-startup) — said as unknown, never as healthy.
        match spt_daemon::read_pump_heartbeat() {
            Some(at_ms) => {
                let age = now_ms().saturating_sub(at_ms);
                let stale_after = 3 * spt_daemon::DaemonConfig::load()
                    .registry_pump_period
                    .as_millis() as u64;
                if age <= stale_after {
                    println!("peer pump: live (last tick {}s ago)", age / 1000);
                } else {
                    println!(
                        "peer pump: STALLED (last tick {}s ago — advertising/connecting \
                         is not running; restart the daemon)",
                        age / 1000
                    );
                }
            }
            None => println!("peer pump: no heartbeat recorded"),
        }
    } else {
        println!("daemon: not running (`spt daemon run`, or any spt command auto-starts it)");
    }

    // Member subnets (names + counts — the bare-subnet status rows).
    let store = SubnetStore::load();
    if store.subnets.is_empty() {
        println!("subnets: (none — this node is standalone)");
    } else {
        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
        let trust = spt_store::trust::TrustStore::load();
        let own_hex = spt_store::nodeid::load_or_create()
            .map(|i| i.public_key().to_hex())
            .unwrap_or_default();
        println!("subnets:");
        for row in subnet_status_rows(
            &store,
            &regs,
            &trust,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            None,
        ) {
            println!(
                "  {}  nodes={} endpoints={}",
                row.name, row.nodes, row.endpoints
            );
        }
    }

    // Local endpoints (the perch roster).
    let perches = roster::enumerate();
    if perches.is_empty() {
        println!("local endpoints: (none)");
    } else {
        println!("local endpoints:");
        for p in perches {
            println!(
                "  {:<28} {:<14} ready={} alive={}",
                p.id, p.state, p.ready, p.alive
            );
        }
    }
    0
}

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

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

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

+/// Run the per-machine daemon (`spt daemon run` — the ex-hidden `spt daemon`

+/// verb, M8 decision 5). Blocks serving the broker + seed-control channels

+/// until killed; a bind contention (another daemon already owns this home)

+/// `spt daemon stop` — graceful daemon shutdown over the seed-control

+/// channel (M8 decision 5, REQ-CLI-2): the daemon acks, then exits its

+/// foreground loop. Idempotent UX: a daemon that is not running is already

+        eprintln!("DAEMON_NOT_RUNNING (already stopped)");

+            eprintln!("DAEMON_STOPPED");

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

+/// `spt daemon [status]` — the node status view (M8 decisions 23 + 25b,

+/// REQ-CLI-2): daemon state (running/stopped + the peer-pump heartbeat —

+/// a stalled pump renders honestly, never implied-healthy), the member

+/// subnets, and this node's local endpoints. Read-only and ungated.

+    // Daemon state: the socket ping is liveness truth (never the pid file).

+    let running = spt_daemon::is_running();

+        let pid = std::fs::read_to_string(spt_daemon::daemon_pid_path())

+            .map(|s| s.trim().to_string());

+            Some(p) if !p.is_empty() => println!("daemon: running (pid {p})"),

+            _ => println!("daemon: running"),

+        // Pump liveness (M8 decision 23 leg 1): the heartbeat file the pump

+        // writes each tick (D4). Absent = no heartbeat written yet (an old

+        // daemon, or one mid-startup) — said as unknown, never as healthy.

+        match spt_daemon::read_pump_heartbeat() {

+                let age = now_ms().saturating_sub(at_ms);

+                let stale_after = 3 * spt_daemon::DaemonConfig::load()

+                    .registry_pump_period

+                    .as_millis() as u64;

+                if age <= stale_after {

+                    println!("peer pump: live (last tick {}s ago)", age / 1000);

+                        "peer pump: STALLED (last tick {}s ago — advertising/connecting \

+                         is not running; restart the daemon)",

+            None => println!("peer pump: no heartbeat recorded"),

+        println!("daemon: not running (`spt daemon run`, or any spt command auto-starts it)");

+    // Member subnets (names + counts — the bare-subnet status rows).

+        println!("subnets: (none — this node is standalone)");

+        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));

+        let trust = spt_store::trust::TrustStore::load();

+        let own_hex = spt_store::nodeid::load_or_create()

+            .map(|i| i.public_key().to_hex())

+            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),

+                "  {}  nodes={} endpoints={}",

+                row.name, row.nodes, row.endpoints

+    // Local endpoints (the perch roster).

+        println!("local endpoints: (none)");

+                p.id, p.state, p.ready, p.alive

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


/// Resolve which subnet a notif targets: an explicit `--subnet` must be a
/// member; with no flag the node's sole subnet is used; several ⇒ refuse and
/// force qualification (no guessing — the D3c/`show-code` stance).
// [impl->REQ-NOTIF-2]
fn resolve_notif_subnet(store: &SubnetStore, flag: Option<String>) -> Result<String, i32> {
    match flag {
        Some(name) => {
            if store.find(&name).is_some() {
                Ok(name)
            } else {
                eprintln!("NO_SUBNET:{name} — this node is not a member");
                Err(1)
            }
        }
        None => match store.subnets.len() {
            0 => {
                eprintln!("NO_SUBNET: this node holds no subnet — pair first");
                Err(1)
            }
            1 => Ok(store.subnets[0].name.clone()),
            _ => {
                eprintln!(
                    "AMBIGUOUS_SUBNET: this node holds several — pass --subnet <name> (no guessing)"
                );
                Err(1)
            }
        },
    }
}

/// `spt notify` — agent-issued subnet notif (ADR-0007, REQ-NOTIF-2): produce
/// into the replicated spool (id minted from this node's EpochSource), then
/// first-fire at the user's most-recently-active visible endpoint. The
/// envelope `from` carries issuer endpoint **and node** (`<id>@<node-hex>`);
/// the subnet rides the envelope's own attribute.
// [impl->REQ-NOTIF-2]
fn cmd_notify(body: Option<String>, subnet: Option<String>, from: Option<String>) -> i32 {
    let issuer = from.or_else(roster::detect_self_id).unwrap_or_default();
    if issuer.is_empty() {
        eprintln!("NO_SELF: notify needs an issuer (--from <id> or $OWL_SESSION_ID)");
        return 1;
    }
    let subnets = SubnetStore::load();
    let subnet = match resolve_notif_subnet(&subnets, subnet) {
        Ok(s) => s,
        Err(code) => return code,
    };

/// Resolve which subnet a notif targets (M8 decision 25a): an explicit
/// `--target` must be a member; with no flag the calling endpoint's HOME
/// subnet is used — a defined default, never a guess; no resolvable home ⇒
/// refuse with the flag named. Pure over its inputs for the decision-table
/// unit test.
// [impl->REQ-NOTIF-2]
fn resolve_notify_subnet(
    store: &SubnetStore,
    target: Option<String>,
    home: Option<String>,
) -> Result<String, i32> {
    match target {
        Some(name) => {
            if store.find(&name).is_some() {
                Ok(name)
            } else {
                eprintln!("NO_SUBNET:{name} — this node is not a member");
                Err(1)
            }
        }
        None => match home {
            Some(h) if store.find(&h).is_some() => Ok(h),
            Some(h) => {
                eprintln!(
                    "HOME_NOT_MEMBER:{h} — the issuer's home subnet is not held on this \
                     node; pass --target <subnet>"
                );
                Err(1)
            }
            None => {
                eprintln!(
                    "NO_HOME_SUBNET: the issuer has no home subnet — pass --target <subnet>"
                );
                Err(1)
            }
        },
    }
}

/// The home-resolution impure half (M8 decision 25a): the issuer's own
/// `info.json` `home_subnet`, or — for a shell perch, which carries no home
/// of its own — the OWNER endpoint's ("the endpoint the calling shell is
/// attached to").
fn issuer_home_subnet(issuer: &str) -> Option<String> {
    let perch_path = perch::resolve_perch_path(issuer, ParentHint::Infer);
    if let Some(rec) = spt_store::info::read_info(&perch_path) {
        if rec.home_subnet.is_some() {
            return rec.home_subnet;
        }
    }
    let owner = spt_store::shellinfo::read_shell_info(&perch_path)?.owner;
    spt_store::info::read_info(&perch::resolve_perch_path(&owner, ParentHint::Infer))?.home_subnet
}

/// `spt subnet notify` — agent-issued subnet notif (ADR-0007, REQ-NOTIF-2;
/// surface moved under the subnet noun, M8 decision 4): produce into the
/// replicated spool (id minted from this node's EpochSource), then
/// first-fire at the user's most-recently-active visible endpoint. The
/// envelope `from` carries issuer endpoint **and node** (`<id>@<node-hex>`);
/// the subnet rides the envelope's own attribute.
// [impl->REQ-NOTIF-2]
fn cmd_notify(body: Option<String>, target: Option<String>, from: Option<String>) -> i32 {
    let issuer = from.or_else(roster::detect_self_id).unwrap_or_default();
    if issuer.is_empty() {
        eprintln!("NO_SELF: notify needs an issuer (--from <id> or $OWL_SESSION_ID)");
        return 1;
    }
    let subnets = SubnetStore::load();
    let home = issuer_home_subnet(&issuer);
    let subnet = match resolve_notify_subnet(&subnets, target, home) {
        Ok(s) => s,
        Err(code) => return code,
    };


/// Resolve which subnet a notif targets: an explicit `--subnet` must be a
/// member; with no flag the node's sole subnet is used; several ⇒ refuse and
/// force qualification (no guessing — the D3c/`show-code` stance).
// [impl->REQ-NOTIF-2]
fn resolve_notif_subnet(store: &SubnetStore, flag: Option<String>) -> Result<String, i32> {
    match flag {
        Some(name) => {
            if store.find(&name).is_some() {
                Ok(name)
            } else {
                eprintln!("NO_SUBNET:{name} — this node is not a member");
                Err(1)
            }
        }
        None => match store.subnets.len() {
            0 => {
                eprintln!("NO_SUBNET: this node holds no subnet — pair first");
                Err(1)
            }
            1 => Ok(store.subnets[0].name.clone()),
            _ => {
                eprintln!(
                    "AMBIGUOUS_SUBNET: this node holds several — pass --subnet <name> (no guessing)"
                );
                Err(1)
            }
        },
    }
}

/// `spt notify` — agent-issued subnet notif (ADR-0007, REQ-NOTIF-2): produce
/// into the replicated spool (id minted from this node's EpochSource), then
/// first-fire at the user's most-recently-active visible endpoint. The
/// envelope `from` carries issuer endpoint **and node** (`<id>@<node-hex>`);
/// the subnet rides the envelope's own attribute.
// [impl->REQ-NOTIF-2]
fn cmd_notify(body: Option<String>, subnet: Option<String>, from: Option<String>) -> i32 {
    let issuer = from.or_else(roster::detect_self_id).unwrap_or_default();
    if issuer.is_empty() {
        eprintln!("NO_SELF: notify needs an issuer (--from <id> or $OWL_SESSION_ID)");
        return 1;
    }
    let subnets = SubnetStore::load();
    let subnet = match resolve_notif_subnet(&subnets, subnet) {
        Ok(s) => s,
        Err(code) => return code,
    };

/// Resolve which subnet a notif targets (M8 decision 25a): an explicit
/// `--target` must be a member; with no flag the calling endpoint's HOME
/// subnet is used — a defined default, never a guess; no resolvable home ⇒
/// refuse with the flag named. Pure over its inputs for the decision-table
/// unit test.
// [impl->REQ-NOTIF-2]
fn resolve_notify_subnet(
    store: &SubnetStore,
    target: Option<String>,
    home: Option<String>,
) -> Result<String, i32> {
    match target {
        Some(name) => {
            if store.find(&name).is_some() {
                Ok(name)
            } else {
                eprintln!("NO_SUBNET:{name} — this node is not a member");
                Err(1)
            }
        }
        None => match home {
            Some(h) if store.find(&h).is_some() => Ok(h),
            Some(h) => {
                eprintln!(
                    "HOME_NOT_MEMBER:{h} — the issuer's home subnet is not held on this \
                     node; pass --target <subnet>"
                );
                Err(1)
            }
            None => {
                eprintln!(
                    "NO_HOME_SUBNET: the issuer has no home subnet — pass --target <subnet>"
                );
                Err(1)
            }
        },
    }
}

/// The home-resolution impure half (M8 decision 25a): the issuer's own
/// `info.json` `home_subnet`, or — for a shell perch, which carries no home
/// of its own — the OWNER endpoint's ("the endpoint the calling shell is
/// attached to").
fn issuer_home_subnet(issuer: &str) -> Option<String> {
    let perch_path = perch::resolve_perch_path(issuer, ParentHint::Infer);
    if let Some(rec) = spt_store::info::read_info(&perch_path) {
        if rec.home_subnet.is_some() {
            return rec.home_subnet;
        }
    }
    let owner = spt_store::shellinfo::read_shell_info(&perch_path)?.owner;
    spt_store::info::read_info(&perch::resolve_perch_path(&owner, ParentHint::Infer))?.home_subnet
}

/// `spt subnet notify` — agent-issued subnet notif (ADR-0007, REQ-NOTIF-2;
/// surface moved under the subnet noun, M8 decision 4): produce into the
/// replicated spool (id minted from this node's EpochSource), then
/// first-fire at the user's most-recently-active visible endpoint. The
/// envelope `from` carries issuer endpoint **and node** (`<id>@<node-hex>`);
/// the subnet rides the envelope's own attribute.
// [impl->REQ-NOTIF-2]
fn cmd_notify(body: Option<String>, target: Option<String>, from: Option<String>) -> i32 {
    let issuer = from.or_else(roster::detect_self_id).unwrap_or_default();
    if issuer.is_empty() {
        eprintln!("NO_SELF: notify needs an issuer (--from <id> or $OWL_SESSION_ID)");
        return 1;
    }
    let subnets = SubnetStore::load();
    let home = issuer_home_subnet(&issuer);
    let subnet = match resolve_notify_subnet(&subnets, target, home) {
        Ok(s) => s,
        Err(code) => return code,
    };

-/// Resolve which subnet a notif targets: an explicit `--subnet` must be a

-/// member; with no flag the node's sole subnet is used; several ⇒ refuse and

-/// force qualification (no guessing — the D3c/`show-code` stance).

+/// Resolve which subnet a notif targets (M8 decision 25a): an explicit

+/// `--target` must be a member; with no flag the calling endpoint's HOME

+/// subnet is used — a defined default, never a guess; no resolvable home ⇒

+/// refuse with the flag named. Pure over its inputs for the decision-table

-fn resolve_notif_subnet(store: &SubnetStore, flag: Option<String>) -> Result<String, i32> {

-        None => match store.subnets.len() {

-                eprintln!("NO_SUBNET: this node holds no subnet — pair first");

+            Some(h) if store.find(&h).is_some() => Ok(h),

+                    "HOME_NOT_MEMBER:{h} — the issuer's home subnet is not held on this \

+                     node; pass --target <subnet>"

-            1 => Ok(store.subnets[0].name.clone()),

-                    "AMBIGUOUS_SUBNET: this node holds several — pass --subnet <name> (no guessing)"

+                    "NO_HOME_SUBNET: the issuer has no home subnet — pass --target <subnet>"

-/// `spt notify` — agent-issued subnet notif (ADR-0007, REQ-NOTIF-2): produce

-/// into the replicated spool (id minted from this node's EpochSource), then

+/// The home-resolution impure half (M8 decision 25a): the issuer's own

+/// `info.json` `home_subnet`, or — for a shell perch, which carries no home

+/// of its own — the OWNER endpoint's ("the endpoint the calling shell is

+fn issuer_home_subnet(issuer: &str) -> Option<String> {

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

+    if let Some(rec) = spt_store::info::read_info(&perch_path) {

+    let owner = spt_store::shellinfo::read_shell_info(&perch_path)?.owner;

+    spt_store::info::read_info(&perch::resolve_perch_path(&owner, ParentHint::Infer))?.home_subnet

+/// `spt subnet notify` — agent-issued subnet notif (ADR-0007, REQ-NOTIF-2;

+/// surface moved under the subnet noun, M8 decision 4): produce into the

+/// replicated spool (id minted from this node's EpochSource), then

-fn cmd_notify(body: Option<String>, subnet: Option<String>, from: Option<String>) -> i32 {

+fn cmd_notify(body: Option<String>, target: Option<String>, from: Option<String>) -> i32 {

-    let subnet = match resolve_notif_subnet(&subnets, subnet) {

+    let home = issuer_home_subnet(&issuer);

+    let subnet = match resolve_notify_subnet(&subnets, target, home) {


Now spt-daemon: stop op + heartbeat reader.


1	//! Canonical per-machine daemon endpoint names + pid path (B6).
2	//!
3	//! The daemon binds two local-IPC listeners — the broker (PTY hosting, B1/B2)
4	//! and the seed-control channel (B6) — under names derived from the active
5	//! `spt_home`. Deriving from `spt_home` keeps each isolated home (a test's
6	//! `SPT_HOME` override, a per-user install) on its own daemon, so parallel test
7	//! processes never collide on one machine while a real install gets one stable
8	//! per-home daemon. The auto-start spawn inherits `SPT_HOME`, so the spawned
9	//! daemon computes the **same** names as the client that started it — that
10	//! shared derivation is the whole of the rendezvous (no registry lookup).
11	// [impl->REQ-DAEMON-3]
12
13	use std::hash::{Hash, Hasher};
14	use std::path::PathBuf;
15
16	use spt_store::perch::spt_home;
17
18	/// A deterministic short tag for the active spt home. `DefaultHasher::new()` is
19	/// seeded with fixed keys (it is **not** randomized like `RandomState`), so the
20	/// tag is stable across processes that share the same `spt_home` — exactly what
21	/// the auto-start handshake needs (the spawned daemon must independently derive
22	/// the launching client's socket name).
23	fn home_tag() -> String {
24	    let mut h = std::collections::hash_map::DefaultHasher::new();
25	    spt_home().hash(&mut h);
26	    format!("{:016x}", h.finish())
27	}
28
29	/// The seed-control listener name (B6): the channel `api seed`/`api listen`
30	/// connect to for the in-memory seed map. It is also the daemon's liveness
31	/// anchor — present for the daemon's whole lifetime.
32	pub fn seed_socket_name() -> String {
33	    format!("spt-daemon-seed-{}.sock", home_tag())
34	}
35
36	/// The broker listener name (B1/B2): PTY hosting + gapless handoff.
37	pub fn broker_socket_name() -> String {
38	    format!("spt-daemon-broker-{}.sock", home_tag())
39	}
40
41	/// The digest-control listener name (B8): the channel `spt digest <id>` connects
42	/// to for a snapshot pull or a delta-stream subscribe (ADR-0008 / REQ-TERM-4).
43	pub fn digest_socket_name() -> String {
44	    format!("spt-daemon-digest-{}.sock", home_tag())
45	}
46
47	/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the
48	/// running daemon writes on start — discoverable for management/cleanup. Not a
49	/// liveness signal (liveness is the seed-control ping); a stale pid here is
50	/// harmless because callers probe the socket, never this file.
51	pub fn daemon_pid_path() -> PathBuf {
52	    spt_home().join("daemon.pid")
53	}
54

//! Canonical per-machine daemon endpoint names + pid path (B6).
//!
//! The daemon binds two local-IPC listeners — the broker (PTY hosting, B1/B2)
//! and the seed-control channel (B6) — under names derived from the active
//! `spt_home`. Deriving from `spt_home` keeps each isolated home (a test's
//! `SPT_HOME` override, a per-user install) on its own daemon, so parallel test
//! processes never collide on one machine while a real install gets one stable
//! per-home daemon. The auto-start spawn inherits `SPT_HOME`, so the spawned
//! daemon computes the **same** names as the client that started it — that
//! shared derivation is the whole of the rendezvous (no registry lookup).
// [impl->REQ-DAEMON-3]

use std::hash::{Hash, Hasher};
use std::path::PathBuf;

use spt_store::perch::spt_home;

/// A deterministic short tag for the active spt home. `DefaultHasher::new()` is
/// seeded with fixed keys (it is **not** randomized like `RandomState`), so the
/// tag is stable across processes that share the same `spt_home` — exactly what
/// the auto-start handshake needs (the spawned daemon must independently derive
/// the launching client's socket name).
fn home_tag() -> String {
    let mut h = std::collections::hash_map::DefaultHasher::new();
    spt_home().hash(&mut h);
    format!("{:016x}", h.finish())
}

/// The seed-control listener name (B6): the channel `api seed`/`api listen`
/// connect to for the in-memory seed map. It is also the daemon's liveness
/// anchor — present for the daemon's whole lifetime.
pub fn seed_socket_name() -> String {
    format!("spt-daemon-seed-{}.sock", home_tag())
}

/// The broker listener name (B1/B2): PTY hosting + gapless handoff.
pub fn broker_socket_name() -> String {
    format!("spt-daemon-broker-{}.sock", home_tag())
}

/// The digest-control listener name (B8): the channel `spt digest <id>` connects
/// to for a snapshot pull or a delta-stream subscribe (ADR-0008 / REQ-TERM-4).
pub fn digest_socket_name() -> String {
    format!("spt-daemon-digest-{}.sock", home_tag())
}

/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the
/// running daemon writes on start — discoverable for management/cleanup. Not a
/// liveness signal (liveness is the seed-control ping); a stale pid here is
/// harmless because callers probe the socket, never this file.
pub fn daemon_pid_path() -> PathBuf {
    spt_home().join("daemon.pid")
}


/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the
/// running daemon writes on start — discoverable for management/cleanup. Not a
/// liveness signal (liveness is the seed-control ping); a stale pid here is
/// harmless because callers probe the socket, never this file.
pub fn daemon_pid_path() -> PathBuf {
    spt_home().join("daemon.pid")
}

/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the
/// running daemon writes on start — discoverable for management/cleanup. Not a
/// liveness signal (liveness is the seed-control ping); a stale pid here is
/// harmless because callers probe the socket, never this file.
pub fn daemon_pid_path() -> PathBuf {
    spt_home().join("daemon.pid")
}

/// Where the peer pump records its last-tick heartbeat (epoch ms, M8
/// decision 23 / REQ-DAEMON-5): written each pump tick (D4), read by
/// `spt daemon status` so a half-dead daemon (pump stalled, dispatcher
/// alive — the 2026-06-07 hfenduleam shape) renders honestly.
pub fn pump_heartbeat_path() -> PathBuf {
    spt_home().join("identity").join("pump-heartbeat.json")
}

/// Read the pump's last-tick heartbeat (epoch ms). `None` = never written
/// (a pre-M8 daemon, or one that has not completed a first tick).
// [impl->REQ-CLI-2]
pub fn read_pump_heartbeat() -> Option<u64> {
    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;
    serde_json::from_str::<u64>(s.trim()).ok()
}


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

/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the
/// running daemon writes on start — discoverable for management/cleanup. Not a
/// liveness signal (liveness is the seed-control ping); a stale pid here is
/// harmless because callers probe the socket, never this file.
pub fn daemon_pid_path() -> PathBuf {
    spt_home().join("daemon.pid")
}

/// Where the peer pump records its last-tick heartbeat (epoch ms, M8
/// decision 23 / REQ-DAEMON-5): written each pump tick (D4), read by
/// `spt daemon status` so a half-dead daemon (pump stalled, dispatcher
/// alive — the 2026-06-07 hfenduleam shape) renders honestly.
pub fn pump_heartbeat_path() -> PathBuf {
    spt_home().join("identity").join("pump-heartbeat.json")
}

/// Read the pump's last-tick heartbeat (epoch ms). `None` = never written
/// (a pre-M8 daemon, or one that has not completed a first tick).
// [impl->REQ-CLI-2]
pub fn read_pump_heartbeat() -> Option<u64> {
    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;
    serde_json::from_str::<u64>(s.trim()).ok()
}

//! Canonical per-machine daemon endpoint names + pid path (B6).
//!
//! The daemon binds two local-IPC listeners — the broker (PTY hosting, B1/B2)
//! and the seed-control channel (B6) — under names derived from the active
//! `spt_home`. Deriving from `spt_home` keeps each isolated home (a test's
//! `SPT_HOME` override, a per-user install) on its own daemon, so parallel test
//! processes never collide on one machine while a real install gets one stable
//! per-home daemon. The auto-start spawn inherits `SPT_HOME`, so the spawned
//! daemon computes the **same** names as the client that started it — that
//! shared derivation is the whole of the rendezvous (no registry lookup).
// [impl->REQ-DAEMON-3]

use std::hash::{Hash, Hasher};
use std::path::PathBuf;

use spt_store::perch::spt_home;

/// A deterministic short tag for the active spt home. `DefaultHasher::new()` is
/// seeded with fixed keys (it is **not** randomized like `RandomState`), so the
/// tag is stable across processes that share the same `spt_home` — exactly what
/// the auto-start handshake needs (the spawned daemon must independently derive
/// the launching client's socket name).
fn home_tag() -> String {
    let mut h = std::collections::hash_map::DefaultHasher::new();
    spt_home().hash(&mut h);
    format!("{:016x}", h.finish())
}

/// The seed-control listener name (B6): the channel `api seed`/`api listen`
/// connect to for the in-memory seed map. It is also the daemon's liveness
/// anchor — present for the daemon's whole lifetime.
pub fn seed_socket_name() -> String {
    format!("spt-daemon-seed-{}.sock", home_tag())
}

/// The broker listener name (B1/B2): PTY hosting + gapless handoff.
pub fn broker_socket_name() -> String {
    format!("spt-daemon-broker-{}.sock", home_tag())
}

/// The digest-control listener name (B8): the channel `spt digest <id>` connects
/// to for a snapshot pull or a delta-stream subscribe (ADR-0008 / REQ-TERM-4).
pub fn digest_socket_name() -> String {
    format!("spt-daemon-digest-{}.sock", home_tag())
}

/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the
/// running daemon writes on start — discoverable for management/cleanup. Not a
/// liveness signal (liveness is the seed-control ping); a stale pid here is
/// harmless because callers probe the socket, never this file.
pub fn daemon_pid_path() -> PathBuf {
    spt_home().join("daemon.pid")
}

+/// Where the peer pump records its last-tick heartbeat (epoch ms, M8

+/// decision 23 / REQ-DAEMON-5): written each pump tick (D4), read by

+/// `spt daemon status` so a half-dead daemon (pump stalled, dispatcher

+/// alive — the 2026-06-07 hfenduleam shape) renders honestly.

+pub fn pump_heartbeat_path() -> PathBuf {

+    spt_home().join("identity").join("pump-heartbeat.json")

+/// Read the pump's last-tick heartbeat (epoch ms). `None` = never written

+/// (a pre-M8 daemon, or one that has not completed a first tick).

+pub fn read_pump_heartbeat() -> Option<u64> {

+    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;

+    serde_json::from_str::<u64>(s.trim()).ok()


Now seedmap stop op:


/// Client→daemon: liveness probe (the auto-start readiness check).
pub const KIND_PING: &str = "ping";
/// Daemon→client: liveness ack.
pub const KIND_PONG: &str = "pong";

/// Client→daemon: liveness probe (the auto-start readiness check).
pub const KIND_PING: &str = "ping";
/// Daemon→client: liveness ack.
pub const KIND_PONG: &str = "pong";
/// Client→daemon: stop serving (graceful daemon shutdown — `spt daemon stop`,
/// M8 decision 5 / REQ-CLI-2).
pub const KIND_STOP: &str = "stop";
/// Daemon→client: stop acknowledged; the serve loop returns after this ack.
pub const KIND_STOPPING: &str = "stopping";


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

/// Client→daemon: liveness probe (the auto-start readiness check).
pub const KIND_PING: &str = "ping";
/// Daemon→client: liveness ack.
pub const KIND_PONG: &str = "pong";
/// Client→daemon: stop serving (graceful daemon shutdown — `spt daemon stop`,
/// M8 decision 5 / REQ-CLI-2).
pub const KIND_STOP: &str = "stop";
/// Daemon→client: stop acknowledged; the serve loop returns after this ack.
pub const KIND_STOPPING: &str = "stopping";

+/// Client→daemon: stop serving (graceful daemon shutdown — `spt daemon stop`,

+/// Daemon→client: stop acknowledged; the serve loop returns after this ack.

+pub const KIND_STOPPING: &str = "stopping";

/// `seed_take` payload — which harness pid's seed to consume.

#[derive(Debug, Clone, Serialize, Deserialize)]


pub fn serve_seed_control(name: &str, reg: Arc<SeedRegistry>) -> io::Result<()> {
    let listener = LocalSocketTransport::bind(name)?;
    loop {
        let conn = listener.accept()?;
        let reg = Arc::clone(&reg);
        thread::spawn(move || {
            let _ = handle_conn(conn, &reg);
        });
    }
}

/// Drive one control connection: handshake, then service its op frames until it
/// disconnects.
fn handle_conn<C: Read + Write>(mut conn: C, reg: &SeedRegistry) -> io::Result<()> {
    // A control client announces itself as a brain (a transient logic client of
    // the daemon); the version handshake gives the same forward-compat as the
    // broker IPC.
    recv_hello(&mut conn, Role::Brain)?;
    loop {
        let env = match read_frame(&mut conn) {
            Ok(e) => e,
            Err(_) => return Ok(()), // peer hung up — connection done
        };
        match env.kind.as_str() {
            KIND_SEED_PUT => {
                let seed: Seed = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let echo = seed.clone();
                reg.put(seed);
                // Echo the stored seed so the client knows the put landed.
                write_frame(&mut conn, &result_frame(Some(echo)))?;
            }
            KIND_SEED_TAKE => {
                let req: TakeReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                write_frame(&mut conn, &result_frame(reg.take(req.parent_pid)))?;
            }
            KIND_PING => {
                write_frame(

pub fn serve_seed_control(name: &str, reg: Arc<SeedRegistry>) -> io::Result<()> {
    use std::sync::atomic::{AtomicBool, Ordering};
    let listener = LocalSocketTransport::bind(name)?;
    // The graceful-stop latch (M8 decision 5 / REQ-CLI-2): a stop handler
    // raises it, then dials one throwaway connection so the blocked accept
    // wakes and observes the flag — the serve loop returns instead of the
    // process being killed, so every caller (daemon, in-process tests) gets
    // a clean teardown.
    let stop = Arc::new(AtomicBool::new(false));
    loop {
        let conn = listener.accept()?;
        if stop.load(Ordering::SeqCst) {
            return Ok(());
        }
        let reg = Arc::clone(&reg);
        let stop = Arc::clone(&stop);
        let name = name.to_string();
        thread::spawn(move || {
            let _ = handle_conn(conn, &reg, &stop, &name);
        });
    }
}

/// Drive one control connection: handshake, then service its op frames until it
/// disconnects.
fn handle_conn<C: Read + Write>(
    mut conn: C,
    reg: &SeedRegistry,
    stop: &std::sync::atomic::AtomicBool,
    name: &str,
) -> io::Result<()> {
    // A control client announces itself as a brain (a transient logic client of
    // the daemon); the version handshake gives the same forward-compat as the
    // broker IPC.
    recv_hello(&mut conn, Role::Brain)?;
    loop {
        let env = match read_frame(&mut conn) {
            Ok(e) => e,
            Err(_) => return Ok(()), // peer hung up — connection done
        };
        match env.kind.as_str() {
            KIND_SEED_PUT => {
                let seed: Seed = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let echo = seed.clone();
                reg.put(seed);
                // Echo the stored seed so the client knows the put landed.
                write_frame(&mut conn, &result_frame(Some(echo)))?;
            }
            KIND_SEED_TAKE => {
                let req: TakeReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                write_frame(&mut conn, &result_frame(reg.take(req.parent_pid)))?;
            }
            KIND_STOP => {
                // Ack FIRST so the client's bounded read completes, then
                // latch + wake the accept loop with a throwaway dial.
                write_frame(
                    &mut conn,
                    &Envelope::new(KIND_STOPPING, serde_json::Value::Null),
                )?;
                stop.store(true, std::sync::atomic::Ordering::SeqCst);
                let _ = LocalSocketTransport::connect(name);
                return Ok(());
            }
            KIND_PING => {
                write_frame(


pub fn serve_seed_control(name: &str, reg: Arc<SeedRegistry>) -> io::Result<()> {
    let listener = LocalSocketTransport::bind(name)?;
    loop {
        let conn = listener.accept()?;
        let reg = Arc::clone(&reg);
        thread::spawn(move || {
            let _ = handle_conn(conn, &reg);
        });
    }
}

/// Drive one control connection: handshake, then service its op frames until it
/// disconnects.
fn handle_conn<C: Read + Write>(mut conn: C, reg: &SeedRegistry) -> io::Result<()> {
    // A control client announces itself as a brain (a transient logic client of
    // the daemon); the version handshake gives the same forward-compat as the
    // broker IPC.
    recv_hello(&mut conn, Role::Brain)?;
    loop {
        let env = match read_frame(&mut conn) {
            Ok(e) => e,
            Err(_) => return Ok(()), // peer hung up — connection done
        };
        match env.kind.as_str() {
            KIND_SEED_PUT => {
                let seed: Seed = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let echo = seed.clone();
                reg.put(seed);
                // Echo the stored seed so the client knows the put landed.
                write_frame(&mut conn, &result_frame(Some(echo)))?;
            }
            KIND_SEED_TAKE => {
                let req: TakeReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                write_frame(&mut conn, &result_frame(reg.take(req.parent_pid)))?;
            }
            KIND_PING => {
                write_frame(

pub fn serve_seed_control(name: &str, reg: Arc<SeedRegistry>) -> io::Result<()> {
    use std::sync::atomic::{AtomicBool, Ordering};
    let listener = LocalSocketTransport::bind(name)?;
    // The graceful-stop latch (M8 decision 5 / REQ-CLI-2): a stop handler
    // raises it, then dials one throwaway connection so the blocked accept
    // wakes and observes the flag — the serve loop returns instead of the
    // process being killed, so every caller (daemon, in-process tests) gets
    // a clean teardown.
    let stop = Arc::new(AtomicBool::new(false));
    loop {
        let conn = listener.accept()?;
        if stop.load(Ordering::SeqCst) {
            return Ok(());
        }
        let reg = Arc::clone(&reg);
        let stop = Arc::clone(&stop);
        let name = name.to_string();
        thread::spawn(move || {
            let _ = handle_conn(conn, &reg, &stop, &name);
        });
    }
}

/// Drive one control connection: handshake, then service its op frames until it
/// disconnects.
fn handle_conn<C: Read + Write>(
    mut conn: C,
    reg: &SeedRegistry,
    stop: &std::sync::atomic::AtomicBool,
    name: &str,
) -> io::Result<()> {
    // A control client announces itself as a brain (a transient logic client of
    // the daemon); the version handshake gives the same forward-compat as the
    // broker IPC.
    recv_hello(&mut conn, Role::Brain)?;
    loop {
        let env = match read_frame(&mut conn) {
            Ok(e) => e,
            Err(_) => return Ok(()), // peer hung up — connection done
        };
        match env.kind.as_str() {
            KIND_SEED_PUT => {
                let seed: Seed = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let echo = seed.clone();
                reg.put(seed);
                // Echo the stored seed so the client knows the put landed.
                write_frame(&mut conn, &result_frame(Some(echo)))?;
            }
            KIND_SEED_TAKE => {
                let req: TakeReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                write_frame(&mut conn, &result_frame(reg.take(req.parent_pid)))?;
            }
            KIND_STOP => {
                // Ack FIRST so the client's bounded read completes, then
                // latch + wake the accept loop with a throwaway dial.
                write_frame(
                    &mut conn,
                    &Envelope::new(KIND_STOPPING, serde_json::Value::Null),
                )?;
                stop.store(true, std::sync::atomic::Ordering::SeqCst);
                let _ = LocalSocketTransport::connect(name);
                return Ok(());
            }
            KIND_PING => {
                write_frame(

/// short connections (ping, then put/take). Thread-per-connection removes that

/// coupling; a failure on one connection never disturbs the others.

+    use std::sync::atomic::{AtomicBool, Ordering};

+    // The graceful-stop latch (M8 decision 5 / REQ-CLI-2): a stop handler

+    // raises it, then dials one throwaway connection so the blocked accept

+    // wakes and observes the flag — the serve loop returns instead of the

+    // process being killed, so every caller (daemon, in-process tests) gets

+    let stop = Arc::new(AtomicBool::new(false));

+        if stop.load(Ordering::SeqCst) {

-            let _ = handle_conn(conn, &reg);

+            let _ = handle_conn(conn, &reg, &stop, &name);

-fn handle_conn<C: Read + Write>(mut conn: C, reg: &SeedRegistry) -> io::Result<()> {

+    stop: &std::sync::atomic::AtomicBool,

+                // Ack FIRST so the client's bounded read completes, then

+                // latch + wake the accept loop with a throwaway dial.

+                    &Envelope::new(KIND_STOPPING, serde_json::Value::Null),

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

+                let _ = LocalSocketTransport::connect(name);


/// Probe whether a daemon is serving the seed-control channel `name` (the
/// auto-start liveness check). `Ok(())` iff a daemon answered the ping.
pub fn ping(name: &str) -> io::Result<()> {

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

/// Probe whether a daemon is serving the seed-control channel `name` (the
/// auto-start liveness check). `Ok(())` iff a daemon answered the ping.
pub fn ping(name: &str) -> io::Result<()> {


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

/// Probe whether a daemon is serving the seed-control channel `name` (the
/// auto-start liveness check). `Ok(())` iff a daemon answered the ping.
pub fn ping(name: &str) -> io::Result<()> {

+/// Ask the daemon serving `name` to stop (graceful: it acks `stopping`, then

+/// its serve loop returns — `spt daemon stop`, M8 decision 5 / REQ-CLI-2).

+pub fn request_stop(name: &str) -> io::Result<()> {

+        &Envelope::new(KIND_STOP, serde_json::Value::Null),

+            io::ErrorKind::InvalidData,


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

    // [unit->REQ-CLI-2] the stop op is acked and ends the serve loop: a
    // post-stop ping finds no listener (graceful teardown, not a kill).
    #[test]
    fn stop_op_acks_then_serve_returns() {
        with_home(|_| {
            let reg = Arc::new(SeedRegistry::new());
            start(Arc::clone(&reg));
            let name = seed_socket_name();

            request_stop(&name).expect("stop acked");
            // The serve loop returns and drops the listener; the socket may
            // take a beat to vanish on some platforms — poll briefly.
            for _ in 0..400 {
                if ping(&name).is_err() {
                    return;
                }
                std::thread::sleep(Duration::from_millis(5));
            }
            panic!("seed daemon still answering after stop");
        });
    }

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


    // [unit->REQ-CLI-2] the stop op is acked and ends the serve loop: a
    // post-stop ping finds no listener (graceful teardown, not a kill).
    #[test]
    fn stop_op_acks_then_serve_returns() {
        with_home(|_| {
            let reg = Arc::new(SeedRegistry::new());
            start(Arc::clone(&reg));
            let name = seed_socket_name();

            request_stop(&name).expect("stop acked");
            // The serve loop returns and drops the listener; the socket may
            // take a beat to vanish on some platforms — poll briefly.
            for _ in 0..400 {
                if ping(&name).is_err() {
                    return;
                }
                std::thread::sleep(Duration::from_millis(5));
            }
            panic!("seed daemon still answering after stop");
        });
    }

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

+    // [unit->REQ-CLI-2] the stop op is acked and ends the serve loop: a

+    // post-stop ping finds no listener (graceful teardown, not a kill).

+    fn stop_op_acks_then_serve_returns() {

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

+            let name = seed_socket_name();

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

+            // The serve loop returns and drops the listener; the socket may

+            // take a beat to vanish on some platforms — poll briefly.

+                if ping(&name).is_err() {

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

+            panic!("seed daemon still answering after stop");


D3 — install & elevation hardening

D5 — docs + CI + acceptance prep


60	                // root would root the user's state universe — a known-torn
61	                // state. ABORT (user-ratified 2026-06-06: refuse, don't limp).
62	                Err(e) => {
63	                    return Err(io::Error::other(format!(
64	                        "DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \
65	                         run spt unelevated"
66	                    )));
67	                }
68	            }
69	        }
70	        #[cfg(windows)]
71	        if crate::deelevate::is_elevated() {
72	            let exe = std::env::current_exe()?;
73	            match crate::deelevate::spawn_deelevated(
74	                &exe.to_string_lossy(),
75	                &["daemon".to_string()],
76	            ) {
77	                Ok(Some(pid)) => {
78	                    eprintln!(
79	                        "DEELEVATED_RESPAWN: unelevated daemon pid {pid}; elevated copy exits"
80	                    );
81	                    return Ok(());
82	                }
83	                // Uniformly-elevated universe (no unelevated identity):
84	                // serving elevated is consistent there. Loud anyway.
85	                Ok(None) => eprintln!(
86	                    "ELEVATED_DAEMON: no unelevated identity to drop to — serving with the \
87	                     elevated token"
88	                ),
89	                // Target existed, respawn failed: an elevated daemon is a
90	                // known-torn state. ABORT (user-ratified 2026-06-06).
91	                Err(e) => {
92	                    return Err(io::Error::other(format!(
93	                        "DEELEVATE_FAIL: {e} — refusing to serve with the elevated token; run \
94	                         spt from an unelevated shell"
95	                    )));
96	                }
97	            }
98	        }
99

                // root would root the user's state universe — a known-torn
                // state. ABORT (user-ratified 2026-06-06: refuse, don't limp).
                Err(e) => {
                    return Err(io::Error::other(format!(
                        "DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \
                         run spt unelevated"
                    )));
                }
            }
        }
        #[cfg(windows)]
        if crate::deelevate::is_elevated() {
            let exe = std::env::current_exe()?;
            match crate::deelevate::spawn_deelevated(
                &exe.to_string_lossy(),
                &["daemon".to_string()],
            ) {
                Ok(Some(pid)) => {
                    eprintln!(
                        "DEELEVATED_RESPAWN: unelevated daemon pid {pid}; elevated copy exits"
                    );
                    return Ok(());
                }
                // Uniformly-elevated universe (no unelevated identity):
                // serving elevated is consistent there. Loud anyway.
                Ok(None) => eprintln!(
                    "ELEVATED_DAEMON: no unelevated identity to drop to — serving with the \
                     elevated token"
                ),
                // Target existed, respawn failed: an elevated daemon is a
                // known-torn state. ABORT (user-ratified 2026-06-06).
                Err(e) => {
                    return Err(io::Error::other(format!(
                        "DEELEVATE_FAIL: {e} — refusing to serve with the elevated token; run \
                         spt from an unelevated shell"
                    )));
                }
            }
        }


            match crate::deelevate::spawn_deelevated(
                &exe.to_string_lossy(),
                &["daemon".to_string()],
            ) {

            match crate::deelevate::spawn_deelevated(
                &exe.to_string_lossy(),
                &["daemon".to_string(), "run".to_string()],
            ) {


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

            match crate::deelevate::spawn_deelevated(
                &exe.to_string_lossy(),
                &["daemon".to_string()],
            ) {

            match crate::deelevate::spawn_deelevated(
                &exe.to_string_lossy(),
                &["daemon".to_string(), "run".to_string()],
            ) {

-                &["daemon".to_string()],

+                &["daemon".to_string(), "run".to_string()],


288	fn spawn_detached() -> io::Result<()> {
289	    let exe = std::env::current_exe()?;
290	    #[cfg(windows)]
291	    {
292	        let exe = exe.to_string_lossy();
293	        let args = ["daemon".to_string()];
294	        match crate::deelevate::spawn_deelevated(&exe, &args) {
295	            Ok(Some(_pid)) => return Ok(()), // de-elevated child running
296	            Ok(None) => {
297	                if crate::deelevate::is_elevated() {
298	                    // No unelevated identity exists (UAC-off admin desktop,
299	                    // SYSTEM): the whole universe is uniformly elevated, so
300	                    // an elevated daemon is consistent there. Loud anyway.
301	                    eprintln!(
302	                        "ELEVATED_DAEMON: no unelevated identity to drop to — daemon inherits \
303	                         the elevated token"
304	                    );
305	                }
306	            }
307	            // A de-elevation TARGET existed but the spawn failed: an
308	            // elevated daemon here is a known-torn state (its pipes deny
309	            // the unelevated clients that target exists for). ABORT — do
310	            // not start a daemon that cannot serve (user-ratified
311	            // 2026-06-06: DEELEVATE_FAIL means spt won't function; refuse,
312	            // don't limp).
313	            Err(e) => {
314	                return Err(io::Error::other(format!(
315	                    "DEELEVATE_FAIL: {e} — refusing to start an ELEVATED daemon (it would deny \
316	                     unelevated clients); run spt from an unelevated shell"
317	                )));
318	            }
319	        }
320	        detached_no_inherit(&exe, &args)?;
321	        Ok(())
322	    }
323	    #[cfg(unix)]
324	    {
325	        use std::os::unix::process::CommandExt;
326	        let mut cmd = Command::new(exe);
327	        cmd.arg("daemon")
328	            .stdin(Stdio::null())
329	            .stdout(Stdio::null())
330	            .stderr(Stdio::null());
331	        if let Some(invoker) = crate::deelevate::sudo_invoker() {
332	            // Drop the child to the sudo invoker: std clears supplementary
333	            // groups and orders setgid-before-setuid when uid is set.
334	            invoker.stamp_env(&mut cmd);
335	            cmd.uid(invoker.uid).gid(invoker.gid);
336	        }
337	        // setsid: detach into a NEW SESSION, not just a new process group —
338	        // a daemon auto-started inside an ssh/non-interactive session
339	        // otherwise dies with it (sshd HUPs the whole session on
340	        // disconnect; observed live in the 2026-06-06 fleet deploy, where
341	        // the freshly-spawned daemon vanished the moment the ssh command
342	        // returned). `process_group(0)` is NOT enough — the new group
343	        // still belongs to the dying session. Safety: this closure only
344	        // calls the async-signal-safe setsid(2).
345	        unsafe {
346	            cmd.pre_exec(|| {
347	                if libc::setsid() == -1 {

fn spawn_detached() -> io::Result<()> {
    let exe = std::env::current_exe()?;
    #[cfg(windows)]
    {
        let exe = exe.to_string_lossy();
        let args = ["daemon".to_string()];
        match crate::deelevate::spawn_deelevated(&exe, &args) {
            Ok(Some(_pid)) => return Ok(()), // de-elevated child running
            Ok(None) => {
                if crate::deelevate::is_elevated() {
                    // No unelevated identity exists (UAC-off admin desktop,
                    // SYSTEM): the whole universe is uniformly elevated, so
                    // an elevated daemon is consistent there. Loud anyway.
                    eprintln!(
                        "ELEVATED_DAEMON: no unelevated identity to drop to — daemon inherits \
                         the elevated token"
                    );
                }
            }
            // A de-elevation TARGET existed but the spawn failed: an
            // elevated daemon here is a known-torn state (its pipes deny
            // the unelevated clients that target exists for). ABORT — do
            // not start a daemon that cannot serve (user-ratified
            // 2026-06-06: DEELEVATE_FAIL means spt won't function; refuse,
            // don't limp).
            Err(e) => {
                return Err(io::Error::other(format!(
                    "DEELEVATE_FAIL: {e} — refusing to start an ELEVATED daemon (it would deny \
                     unelevated clients); run spt from an unelevated shell"
                )));
            }
        }
        detached_no_inherit(&exe, &args)?;
        Ok(())
    }
    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        let mut cmd = Command::new(exe);
        cmd.arg("daemon")
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null());
        if let Some(invoker) = crate::deelevate::sudo_invoker() {
            // Drop the child to the sudo invoker: std clears supplementary
            // groups and orders setgid-before-setuid when uid is set.
            invoker.stamp_env(&mut cmd);
            cmd.uid(invoker.uid).gid(invoker.gid);
        }
        // setsid: detach into a NEW SESSION, not just a new process group —
        // a daemon auto-started inside an ssh/non-interactive session
        // otherwise dies with it (sshd HUPs the whole session on
        // disconnect; observed live in the 2026-06-06 fleet deploy, where
        // the freshly-spawned daemon vanished the moment the ssh command
        // returned). `process_group(0)` is NOT enough — the new group
        // still belongs to the dying session. Safety: this closure only
        // calls the async-signal-safe setsid(2).
        unsafe {
            cmd.pre_exec(|| {
                if libc::setsid() == -1 {


        let exe = exe.to_string_lossy();
        let args = ["daemon".to_string()];
        match crate::deelevate::spawn_deelevated(&exe, &args) {

        let exe = exe.to_string_lossy();
        let args = ["daemon".to_string(), "run".to_string()];
        match crate::deelevate::spawn_deelevated(&exe, &args) {


        let exe = exe.to_string_lossy();
        let args = ["daemon".to_string()];
        match crate::deelevate::spawn_deelevated(&exe, &args) {

        let exe = exe.to_string_lossy();
        let args = ["daemon".to_string(), "run".to_string()];
        match crate::deelevate::spawn_deelevated(&exe, &args) {

-        let args = ["daemon".to_string()];

+        let args = ["daemon".to_string(), "run".to_string()];

Ok(Some(_pid)) => return Ok(()), // de-elevated child running


        let mut cmd = Command::new(exe);
        cmd.arg("daemon")
            .stdin(Stdio::null())

        let mut cmd = Command::new(exe);
        cmd.args(["daemon", "run"])
            .stdin(Stdio::null())


        let mut cmd = Command::new(exe);
        cmd.arg("daemon")
            .stdin(Stdio::null())

        let mut cmd = Command::new(exe);
        cmd.args(["daemon", "run"])
            .stdin(Stdio::null())


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

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

/// Gracefully stop the daemon serving this home (`spt daemon stop`, M8
/// decision 5 / REQ-CLI-2): the seed-control stop op — acked, then the
/// daemon's foreground loop returns and the process exits cleanly.
// [impl->REQ-CLI-2]
pub fn request_stop() -> io::Result<()> {
    seedmap::request_stop(&seed_socket_name())
}


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

/// Gracefully stop the daemon serving this home (`spt daemon stop`, M8
/// decision 5 / REQ-CLI-2): the seed-control stop op — acked, then the
/// daemon's foreground loop returns and the process exits cleanly.
// [impl->REQ-CLI-2]
pub fn request_stop() -> io::Result<()> {
    seedmap::request_stop(&seed_socket_name())
}

+/// Gracefully stop the daemon serving this home (`spt daemon stop`, M8

+/// decision 5 / REQ-CLI-2): the seed-control stop op — acked, then the

+/// daemon's foreground loop returns and the process exits cleanly.

+pub fn request_stop() -> io::Result<()> {

+    seedmap::request_stop(&seed_socket_name())

/// Auto-start (REQ-DAEMON-3): ensure a daemon is up for this home. A no-op if one

/// already answers; otherwise spawn `spt daemon` detached and wait (bounded) for

/// it to bind. Idempotent + race-tolerant: if two callers spawn at once, one


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

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


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

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


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

pub use endpoint::{
    broker_socket_name, daemon_pid_path, digest_socket_name, pump_heartbeat_path,
    read_pump_heartbeat, seed_socket_name,
};