120	        .expect("ready --once");
121	    let stdout = String::from_utf8_lossy(&drained.stdout);
122	    // The backlog path renders the same envelope shape as the live path.
123	    // [int->REQ-MSG-4]
124	    assert!(
125	        stdout.contains(r#"<EVENT type="msg" from="lea">ping while you were away</EVENT>"#),
126	        "backlog must drain enveloped on return, got {stdout:?}"
127	    );
128	}
129
130	/// Run a one-shot spt command expecting exit 0; return its stdout.
131	fn run_ok(home: &std::path::Path, args: &[&str]) -> String {
132	    let out = spt()
133	        .args(args)
134	        .env("SPT_HOME", home)
135	        .output()
136	        .expect("spt runs");
137	    assert!(
138	        out.status.success(),
139	        "`spt {}` failed: {:?}",
140	        args.join(" "),
141	        out
142	    );
143	    String::from_utf8_lossy(&out.stdout).into_owned()
144	}
145
146	/// Run a one-shot spt command with `body` on stdin, returning stdout+stderr.
147	fn send(home: &std::path::Path, args: &[&str], body: &str) -> String {
148	    let mut child = spt()
149	        .args(args)
150	        .env("SPT_HOME", home)
151	        .stdin(Stdio::piped())
152	        .stdout(Stdio::piped())
153	        .stderr(Stdio::piped())
154	        .spawn()
155	        .expect("spawn spt");
156	    {
157	        use std::io::Write;
158	        child
159	            .stdin
160	            .take()
161	            .unwrap()
162	            .write_all(body.as_bytes())
163	            .expect("write body");
164	    }
165	    // Bounded read (KNOWN-HAZARDS 5.6 posture): a hang here means a detached
166	    // child inherited the pipe.
167	    let (tx, rx) = std::sync::mpsc::channel();
168	    std::thread::spawn(move || {
169	        let out = child.wait_with_output();
170	        let _ = tx.send(out);
171	    });
172	    let out = rx
173	        .recv_timeout(Duration::from_secs(30))
174	        .expect("spt call must complete")
175	        .expect("spt runs");
176	    let mut s = String::from_utf8_lossy(&out.stdout).into_owned();
177	    let mut e = String::new();
178	    let _ = std::io::Cursor::new(out.stderr).read_to_string(&mut e);
179	    s.push_str(&e);

        .expect("ready --once");
    let stdout = String::from_utf8_lossy(&drained.stdout);
    // The backlog path renders the same envelope shape as the live path.
    // [int->REQ-MSG-4]
    assert!(
        stdout.contains(r#"<EVENT type="msg" from="lea">ping while you were away</EVENT>"#),
        "backlog must drain enveloped on return, got {stdout:?}"
    );
}

/// Run a one-shot spt command expecting exit 0; return its stdout.
fn run_ok(home: &std::path::Path, args: &[&str]) -> String {
    let out = spt()
        .args(args)
        .env("SPT_HOME", home)
        .output()
        .expect("spt runs");
    assert!(
        out.status.success(),
        "`spt {}` failed: {:?}",
        args.join(" "),
        out
    );
    String::from_utf8_lossy(&out.stdout).into_owned()
}

/// Run a one-shot spt command with `body` on stdin, returning stdout+stderr.
fn send(home: &std::path::Path, args: &[&str], body: &str) -> String {
    let mut child = spt()
        .args(args)
        .env("SPT_HOME", home)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn spt");
    {
        use std::io::Write;
        child
            .stdin
            .take()
            .unwrap()
            .write_all(body.as_bytes())
            .expect("write body");
    }
    // Bounded read (KNOWN-HAZARDS 5.6 posture): a hang here means a detached
    // child inherited the pipe.
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let out = child.wait_with_output();
        let _ = tx.send(out);
    });
    let out = rx
        .recv_timeout(Duration::from_secs(30))
        .expect("spt call must complete")
        .expect("spt runs");
    let mut s = String::from_utf8_lossy(&out.stdout).into_owned();
    let mut e = String::new();
    let _ = std::io::Cursor::new(out.stderr).read_to_string(&mut e);
    s.push_str(&e);


No matches found


<tool_use_error>Path does not exist: xtask\src. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.</tool_use_error>

Error: Path does not exist: xtask\src. Note: your current working directory is C:\Users\decid\Documents\projects\spt-core.


Found 12 files
M8-PLAN.md
docs-site\src\quickstart\messaging.md
crates\xtask\src\main.rs
M7-PLAN.md
docs-site\src\networking\overview.md
docs-site\src\instances\overview.md
docs-site\src\concepts\overview.md
docs-site\src\SUMMARY.md
docs-site\llms.txt
docs-site\src\messaging\overview.md
docs-site\src\index.md
docs-site\src\self-update\overview.md


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.


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


4://! - `gen` — regenerate the checked-in generated docs (CLI reference).
5://! - `check` — drift gate: regenerate + diff the CLI reference, and
63:/// reference platform-dependent).
109:/// The generated CLI reference: every command + nested subcommand's help,
111:/// reference IS the binary's own words — it cannot drift (REQ-DOCS-5).
113:fn cli_reference(bin: &Path) -> String {
115:        "# CLI reference\n\n\
162:    let generated = cli_reference(&bin);
163:    let path = root.join("docs-site/src/cli/reference.md");
166:            .expect("checked-in cli/reference.md (run `cargo run -p xtask -- gen`)")
170:                "xtask check: docs-site/src/cli/reference.md drifted from the \
176:        std::fs::write(&path, &generated).expect("write cli reference");
384:/// Resolve the signing identity. Preference order:


100	                        }
101	                    }
102	                }
103	            }
104	        }
105	    }
106	    cmds
107	}
108
109	/// The generated CLI reference: every command + nested subcommand's help,
110	/// verbatim. Help text is a first-class doc surface (DOCS-STRATEGY), so the
111	/// reference IS the binary's own words — it cannot drift (REQ-DOCS-5).
112	// [impl->REQ-DOCS-5]
113	fn cli_reference(bin: &Path) -> String {
114	    let mut out = String::from(
115	        "# CLI reference\n\n\
116	         > **Generated** from the `spt` binary's own `--help` output \
117	         (`cargo run -p xtask -- gen`) and drift-gated in CI — this page \
118	         cannot disagree with the binary. Do not edit by hand.\n\n",
119	    );
120	    let root_help = help_for(bin, &[]);
121	    out.push_str("## spt\n\n```text\n");
122	    out.push_str(&root_help);
123	    out.push_str("```\n");
124	    for cmd in subcommands(&root_help) {
125	        // `spt api` requires --adapter even for subcommand help; pass a
126	        // stand-in so clap renders the help instead of erroring.
127	        let help = if cmd == "api" {
128	            help_for(bin, &["api", "--adapter", "x"])
129	        } else {
130	            help_for(bin, &[&cmd])
131	        };
132	        out.push_str(&format!("\n## spt {cmd}\n\n```text\n{help}```\n"));
133	        for sub in subcommands(&help) {
134	            let sub_help = if cmd == "api" {
135	                help_for(bin, &["api", "--adapter", "x", &sub])
136	            } else {
137	                help_for(bin, &[&cmd, &sub])
138	            };
139	            out.push_str(&format!(
140	                "\n### spt {cmd} {sub}\n\n```text\n{sub_help}```\n"
141	            ));
142	        }
143	    }
144	    out
145	}
146
147	/// Locate (building if needed) the workspace `spt` binary.
148	fn spt_bin(root: &Path) -> PathBuf {
149	    let status = Command::new("cargo")
150	        .args(["build", "-q", "-p", "spt", "--bin", "spt"])
151	        .current_dir(root)
152	        .status()
153	        .expect("cargo build spt");
154	    assert!(status.success(), "building spt failed");
155	    let name = if cfg!(windows) { "spt.exe" } else { "spt" };
156	    root.join("target/debug").join(name)
157	}
158
159	fn gen(check: bool) {
160	    let root = repo_root();
161	    let bin = spt_bin(&root);
162	    let generated = cli_reference(&bin);
163	    let path = root.join("docs-site/src/cli/reference.md");
164	    if check {
165	        let on_disk = std::fs::read_to_string(&path)
166	            .expect("checked-in cli/reference.md (run `cargo run -p xtask -- gen`)")
167	            .replace("\r\n", "\n");
168	        if on_disk != generated {
169	            eprintln!(
170	                "xtask check: docs-site/src/cli/reference.md drifted from the \
171	                 binary's --help — run `cargo run -p xtask -- gen` and commit"
172	            );
173	            std::process::exit(1);
174	        }
175	    } else {
176	        std::fs::write(&path, &generated).expect("write cli reference");
177	        eprintln!("xtask gen: wrote {}", path.display());
178	    }
179	}
180
181	// ---------------------------------------------------------------- llms
182
183	/// Every relative `.md`/`.json`/`.sh`/`.ps1` link in the curated llms.txt
184	/// must resolve — agents cache links; a dead one is a contract break
185	/// (DOCS-STRATEGY principle 9).
186	fn check_llms_links() {
187	    let root = repo_root();
188	    let llms = std::fs::read_to_string(root.join("docs-site/llms.txt"))
189	        .expect("docs-site/llms.txt (the curated index)");

                        }
                    }
                }
            }
        }
    }
    cmds
}

/// The generated CLI reference: every command + nested subcommand's help,
/// verbatim. Help text is a first-class doc surface (DOCS-STRATEGY), so the
/// reference IS the binary's own words — it cannot drift (REQ-DOCS-5).
// [impl->REQ-DOCS-5]
fn cli_reference(bin: &Path) -> String {
    let mut out = String::from(
        "# CLI reference\n\n\
         > **Generated** from the `spt` binary's own `--help` output \
         (`cargo run -p xtask -- gen`) and drift-gated in CI — this page \
         cannot disagree with the binary. Do not edit by hand.\n\n",
    );
    let root_help = help_for(bin, &[]);
    out.push_str("## spt\n\n```text\n");
    out.push_str(&root_help);
    out.push_str("```\n");
    for cmd in subcommands(&root_help) {
        // `spt api` requires --adapter even for subcommand help; pass a
        // stand-in so clap renders the help instead of erroring.
        let help = if cmd == "api" {
            help_for(bin, &["api", "--adapter", "x"])
        } else {
            help_for(bin, &[&cmd])
        };
        out.push_str(&format!("\n## spt {cmd}\n\n```text\n{help}```\n"));
        for sub in subcommands(&help) {
            let sub_help = if cmd == "api" {
                help_for(bin, &["api", "--adapter", "x", &sub])
            } else {
                help_for(bin, &[&cmd, &sub])
            };
            out.push_str(&format!(
                "\n### spt {cmd} {sub}\n\n```text\n{sub_help}```\n"
            ));
        }
    }
    out
}

/// Locate (building if needed) the workspace `spt` binary.
fn spt_bin(root: &Path) -> PathBuf {
    let status = Command::new("cargo")
        .args(["build", "-q", "-p", "spt", "--bin", "spt"])
        .current_dir(root)
        .status()
        .expect("cargo build spt");
    assert!(status.success(), "building spt failed");
    let name = if cfg!(windows) { "spt.exe" } else { "spt" };
    root.join("target/debug").join(name)
}

fn gen(check: bool) {
    let root = repo_root();
    let bin = spt_bin(&root);
    let generated = cli_reference(&bin);
    let path = root.join("docs-site/src/cli/reference.md");
    if check {
        let on_disk = std::fs::read_to_string(&path)
            .expect("checked-in cli/reference.md (run `cargo run -p xtask -- gen`)")
            .replace("\r\n", "\n");
        if on_disk != generated {
            eprintln!(
                "xtask check: docs-site/src/cli/reference.md drifted from the \
                 binary's --help — run `cargo run -p xtask -- gen` and commit"
            );
            std::process::exit(1);
        }
    } else {
        std::fs::write(&path, &generated).expect("write cli reference");
        eprintln!("xtask gen: wrote {}", path.display());
    }
}

// ---------------------------------------------------------------- llms

/// Every relative `.md`/`.json`/`.sh`/`.ps1` link in the curated llms.txt
/// must resolve — agents cache links; a dead one is a contract break
/// (DOCS-STRATEGY principle 9).
fn check_llms_links() {
    let root = repo_root();
    let llms = std::fs::read_to_string(root.join("docs-site/llms.txt"))
        .expect("docs-site/llms.txt (the curated index)");


Now read seedmap serve loop + daemon.rs spawn sites for stop op + argv fix.


1	//! The in-memory daemon seed map (B6 / REQ-START-3) — the in-memory replacement
2	//! for the retired `spt_store::seed` FILE bridge.
3	//!
4	//! Interim (M1/M2a) harness-hosted startup wrote an ephemeral seed *file* keyed
5	//! by the harness `parent_pid`: `api seed` wrote it, a later `api listen`
6	//! consumed it. M3b retires that file (the file path goes dead): the seed now
7	//! lives in the daemon's memory. `api seed` PUTs it over this seed-control IPC;
8	//! `api listen` TAKEs it (consume-once) by the same `parent_pid`. No file is ever
9	//! written — the acceptance for B6 (an spt-hosted session binds with no seed
10	//! file).
11	//!
12	//! The seed map is ephemeral cross-invocation **binding material** — the
13	//! in-memory analog of the file the daemon would otherwise hold on disk, the
14	//! same category as the accepted-socket bookkeeping the broker already owns
15	//! (ADR-0004 §B), not routing/registry/parse logic. It is intentionally tiny:
16	//! put / take by pid. Because seeds are ephemeral (refreshed on the next
17	//! SessionStart), a daemon restart dropping the map is recoverable — the harness
18	//! re-seeds.
19	//!
20	//! The control protocol rides the same forward-compatible [`Envelope`] + codec +
21	//! version handshake as the broker IPC; a transient `api` CLI connects as a
22	//! [`Role::Brain`] logic client, issues one op, reads the reply, and disconnects.
23	// [impl->REQ-START-3]
24
25	use std::collections::HashMap;
26	use std::io::{self, Read, Write};
27	use std::sync::{Arc, Mutex};
28	use std::thread;
29
30	use serde::{Deserialize, Serialize};
31
32	use spt_store::seed::Seed;
33
34	use crate::codec::{read_frame, write_frame};
35	use crate::frame::{Envelope, Role};
36	use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};
37
38	/// Client→daemon: store (or refresh) a seed in the in-memory map.
39	pub const KIND_SEED_PUT: &str = "seed_put";
40	/// Client→daemon: consume the seed for a `parent_pid` (take-once).
41	pub const KIND_SEED_TAKE: &str = "seed_take";
42	/// Daemon→client: the result of a put/take (the seed, or none).
43	pub const KIND_SEED_RESULT: &str = "seed_result";
44	/// Client→daemon: liveness probe (the auto-start readiness check).
45	pub const KIND_PING: &str = "ping";
46	/// Daemon→client: liveness ack.
47	pub const KIND_PONG: &str = "pong";
48
49	/// `seed_take` payload — which harness pid's seed to consume.
50	#[derive(Debug, Clone, Serialize, Deserialize)]
51	pub struct TakeReq {
52	    /// The stable harness parent pid the seed was keyed under.
53	    pub parent_pid: u32,
54	}
55
56	/// `seed_result` payload — the seed (present on a hit, absent on a miss). A put
57	/// echoes the stored seed so the client knows the daemon processed the frame
58	/// before it closes the connection.
59	#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60	pub struct SeedResult {
61	    /// The seed, when present.
62	    #[serde(default, skip_serializing_if = "Option::is_none")]
63	    pub seed: Option<Seed>,
64	}
65
66	/// The daemon's in-memory seed map, keyed by the stable harness `parent_pid`.
67	#[derive(Default)]
68	pub struct SeedRegistry {
69	    map: Mutex<HashMap<u32, Seed>>,
70	}
71
72	impl SeedRegistry {
73	    /// A fresh, empty registry.
74	    pub fn new() -> Self {
75	        SeedRegistry {
76	            map: Mutex::new(HashMap::new()),
77	        }
78	    }
79
80	    /// Store (or refresh) a seed keyed by its `parent_pid`.
81	    pub fn put(&self, seed: Seed) {
82	        self.map.lock().unwrap().insert(seed.parent_pid, seed);
83	    }
84
85	    /// Consume the seed for `parent_pid` (take-once), if present.
86	    pub fn take(&self, parent_pid: u32) -> Option<Seed> {
87	        self.map.lock().unwrap().remove(&parent_pid)
88	    }
89
90	    /// Peek the seed for `parent_pid` without consuming (introspection / tests).
91	    pub fn peek(&self, parent_pid: u32) -> Option<Seed> {
92	        self.map.lock().unwrap().get(&parent_pid).cloned()
93	    }
94	}
95
96	/// Build a `seed_result` envelope carrying `seed` (or none).
97	fn result_frame(seed: Option<Seed>) -> Envelope {
98	    Envelope::new(
99	        KIND_SEED_RESULT,
100	        serde_json::to_value(SeedResult { seed }).expect("SeedResult serializes"),
101	    )
102	}
103
104	/// Bind the seed-control channel at `name` and serve it forever, dispatching
105	/// against `reg`. Each accepted connection is handled on **its own thread**
106	/// (mirroring the broker): a control client's connection lives until the client
107	/// closes it, and `handle_conn` blocks reading the next frame in the meantime, so
108	/// serving inline would let one open connection stall every other client's
109	/// `accept` — a deadlock that bites because a single `api` call opens several
110	/// short connections (ping, then put/take). Thread-per-connection removes that
111	/// coupling; a failure on one connection never disturbs the others.
112	pub fn serve_seed_control(name: &str, reg: Arc<SeedRegistry>) -> io::Result<()> {
113	    let listener = LocalSocketTransport::bind(name)?;
114	    loop {
115	        let conn = listener.accept()?;
116	        let reg = Arc::clone(&reg);
117	        thread::spawn(move || {
118	            let _ = handle_conn(conn, &reg);
119	        });
120	    }
121	}
122
123	/// Drive one control connection: handshake, then service its op frames until it
124	/// disconnects.
125	fn handle_conn<C: Read + Write>(mut conn: C, reg: &SeedRegistry) -> io::Result<()> {
126	    // A control client announces itself as a brain (a transient logic client of
127	    // the daemon); the version handshake gives the same forward-compat as the
128	    // broker IPC.
129	    recv_hello(&mut conn, Role::Brain)?;
130	    loop {
131	        let env = match read_frame(&mut conn) {
132	            Ok(e) => e,
133	            Err(_) => return Ok(()), // peer hung up — connection done
134	        };
135	        match env.kind.as_str() {
136	            KIND_SEED_PUT => {
137	                let seed: Seed = serde_json::from_value(env.payload)
138	                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
139	                let echo = seed.clone();
140	                reg.put(seed);
141	                // Echo the stored seed so the client knows the put landed.
142	                write_frame(&mut conn, &result_frame(Some(echo)))?;
143	            }
144	            KIND_SEED_TAKE => {
145	                let req: TakeReq = serde_json::from_value(env.payload)
146	                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
147	                write_frame(&mut conn, &result_frame(reg.take(req.parent_pid)))?;
148	            }
149	            KIND_PING => {
150	                write_frame(

//! The in-memory daemon seed map (B6 / REQ-START-3) — the in-memory replacement
//! for the retired `spt_store::seed` FILE bridge.
//!
//! Interim (M1/M2a) harness-hosted startup wrote an ephemeral seed *file* keyed
//! by the harness `parent_pid`: `api seed` wrote it, a later `api listen`
//! consumed it. M3b retires that file (the file path goes dead): the seed now
//! lives in the daemon's memory. `api seed` PUTs it over this seed-control IPC;
//! `api listen` TAKEs it (consume-once) by the same `parent_pid`. No file is ever
//! written — the acceptance for B6 (an spt-hosted session binds with no seed
//! file).
//!
//! The seed map is ephemeral cross-invocation **binding material** — the
//! in-memory analog of the file the daemon would otherwise hold on disk, the
//! same category as the accepted-socket bookkeeping the broker already owns
//! (ADR-0004 §B), not routing/registry/parse logic. It is intentionally tiny:
//! put / take by pid. Because seeds are ephemeral (refreshed on the next
//! SessionStart), a daemon restart dropping the map is recoverable — the harness
//! re-seeds.
//!
//! The control protocol rides the same forward-compatible [`Envelope`] + codec +
//! version handshake as the broker IPC; a transient `api` CLI connects as a
//! [`Role::Brain`] logic client, issues one op, reads the reply, and disconnects.
// [impl->REQ-START-3]

use std::collections::HashMap;
use std::io::{self, Read, Write};
use std::sync::{Arc, Mutex};
use std::thread;

use serde::{Deserialize, Serialize};

use spt_store::seed::Seed;

use crate::codec::{read_frame, write_frame};
use crate::frame::{Envelope, Role};
use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};

/// Client→daemon: store (or refresh) a seed in the in-memory map.
pub const KIND_SEED_PUT: &str = "seed_put";
/// Client→daemon: consume the seed for a `parent_pid` (take-once).
pub const KIND_SEED_TAKE: &str = "seed_take";
/// Daemon→client: the result of a put/take (the seed, or none).
pub const KIND_SEED_RESULT: &str = "seed_result";
/// 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";

/// `seed_take` payload — which harness pid's seed to consume.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TakeReq {
    /// The stable harness parent pid the seed was keyed under.
    pub parent_pid: u32,
}

/// `seed_result` payload — the seed (present on a hit, absent on a miss). A put
/// echoes the stored seed so the client knows the daemon processed the frame
/// before it closes the connection.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeedResult {
    /// The seed, when present.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub seed: Option<Seed>,
}

/// The daemon's in-memory seed map, keyed by the stable harness `parent_pid`.
#[derive(Default)]
pub struct SeedRegistry {
    map: Mutex<HashMap<u32, Seed>>,
}

impl SeedRegistry {
    /// A fresh, empty registry.
    pub fn new() -> Self {
        SeedRegistry {
            map: Mutex::new(HashMap::new()),
        }
    }

    /// Store (or refresh) a seed keyed by its `parent_pid`.
    pub fn put(&self, seed: Seed) {
        self.map.lock().unwrap().insert(seed.parent_pid, seed);
    }

    /// Consume the seed for `parent_pid` (take-once), if present.
    pub fn take(&self, parent_pid: u32) -> Option<Seed> {
        self.map.lock().unwrap().remove(&parent_pid)
    }

    /// Peek the seed for `parent_pid` without consuming (introspection / tests).
    pub fn peek(&self, parent_pid: u32) -> Option<Seed> {
        self.map.lock().unwrap().get(&parent_pid).cloned()
    }
}

/// Build a `seed_result` envelope carrying `seed` (or none).
fn result_frame(seed: Option<Seed>) -> Envelope {
    Envelope::new(
        KIND_SEED_RESULT,
        serde_json::to_value(SeedResult { seed }).expect("SeedResult serializes"),
    )
}

/// Bind the seed-control channel at `name` and serve it forever, dispatching
/// against `reg`. Each accepted connection is handled on **its own thread**
/// (mirroring the broker): a control client's connection lives until the client
/// closes it, and `handle_conn` blocks reading the next frame in the meantime, so
/// serving inline would let one open connection stall every other client's
/// `accept` — a deadlock that bites because a single `api` call opens several
/// short connections (ping, then put/take). Thread-per-connection removes that
/// coupling; a failure on one connection never disturbs the others.
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(


Grep

pub fn daemon_pid_path|pub use|pub fn seed_socket_name|pub fn digest_socket_name|pub fn broker_socket_name


crates\spt-daemon\src\deelevate.rs:39:pub use unix::{sudo_invoker, SudoInvoker};
crates\spt-daemon\src\deelevate.rs:42:pub use windows::{is_elevated, spawn_deelevated};
crates\spt-daemon\src\deelevate.rs:78:        pub user: Option<String>,
crates\spt-daemon\src\endpoint.rs:32:pub fn seed_socket_name() -> String {
crates\spt-daemon\src\endpoint.rs:37:pub fn broker_socket_name() -> String {
crates\spt-daemon\src\endpoint.rs:43:pub fn digest_socket_name() -> String {
crates\spt-daemon\src\endpoint.rs:51:pub fn daemon_pid_path() -> PathBuf {
crates\spt-daemon\src\lib.rs:156:pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
crates\spt-daemon\src\lib.rs:157:pub use adapter_update::{
crates\spt-daemon\src\lib.rs:161:pub use applyhost::{apply_staged, ApplyStagedOutcome};
crates\spt-daemon\src\lib.rs:162:pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
crates\spt-daemon\src\lib.rs:163:pub use brain::{Brain, BrainState, BrokerEvent};
crates\spt-daemon\src\lib.rs:164:pub use broker::Broker;
crates\spt-daemon\src\lib.rs:165:pub use config::{DaemonConfig, RelayChoice};
crates\spt-daemon\src\lib.rs:166:pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
crates\spt-daemon\src\lib.rs:167:pub use daemon::{ensure_running, is_running, Daemon};
crates\spt-daemon\src\lib.rs:168:pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
crates\spt-daemon\src\lib.rs:169:pub use digesthub::{
crates\spt-daemon\src\lib.rs:173:pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
crates\spt-daemon\src\lib.rs:174:pub use endpoint::{broker_socket_name, daemon_pid_path, digest_socket_name, seed_socket_name};
crates\spt-daemon\src\lib.rs:175:pub use frame::{
crates\spt-daemon\src\lib.rs:179:pub use lifecycle::{BrainLifecycle, TickReport};
crates\spt-daemon\src\lib.rs:180:pub use linkhost::{
crates\spt-daemon\src\lib.rs:183:pub use nethost::{NetConfig, NetHost, NET_EFFECT_SESSION};
crates\spt-daemon\src\lib.rs:184:pub use notif::{
crates\spt-daemon\src\lib.rs:189:pub use notifsync::{apply_notif_feed, emit_notif_feed, NotifApplyVerdict, NotifPolicy};
crates\spt-daemon\src\lib.rs:190:pub use propagate::{request_update, serve_update, UpdatePullOutcome, UpdateServeOutcome};
crates\spt-daemon\src\lib.rs:191:pub use psyrelay::{
crates\spt-daemon\src\lib.rs:194:pub use relay::Relay;
crates\spt-daemon\src\lib.rs:195:pub use relcache::ReleaseCache;
crates\spt-daemon\src\lib.rs:196:pub use release::{
crates\spt-daemon\src\lib.rs:202:pub use resthost::{request_rest, serve_rest, RestRequestOutcome, RestServeOutcome};
crates\spt-daemon\src\lib.rs:203:pub use resting::{
crates\spt-daemon\src\lib.rs:208:pub use seedmap::{put_seed, take_seed, SeedRegistry};
crates\spt-daemon\src\lib.rs:209:pub use sync::{
crates\spt-daemon\src\lib.rs:213:pub use transport::{DaemonTransport, LocalSocketTransport};
crates\spt-daemon\src\lib.rs:214:pub use update::{
crates\spt-daemon\src\lib.rs:218:pub use wan::{receive_wan, WanOutcome};
crates\spt-daemon\src\lib.rs:219:pub use xfer::{fetch_file, push_file, serve_xfer, XferServeOutcome};


pub home_subnet|pub owner|pub struct InfoRecord|pub adapter|pub resources


107:    pub resources: Option<String>,
116:    pub home_subnet: Option<String>,
123:    pub adapter: Option<String>,


40	    pub id: String,
41	    /// Creation timestamp (opaque string; producer's format).
42	    pub started: String,
43	    /// Owning process pid, or the `"BUSY"` sentinel.
44	    pub pid: PidValue,
45	    /// Harness session id bound to this perch.
46	    pub session_id: String,
47	    /// Endpoint type tag (`spt_proto::endpoint::EndpointType::as_tag`).
48	    pub state: String,
49	    #[serde(skip_serializing_if = "Option::is_none")]
50	    pub parent_pid: Option<u32>,
51	    /// Working directory the perch was created in (forward-slash normalized).
52	    #[serde(skip_serializing_if = "Option::is_none")]
53	    pub cwd: Option<String>,
54	    /// Daemon-managed liveness status (`online`/`offline`) for **daemon-hosted**
55	    /// perches (Psyche, spt-hosted Self). Present ⇒ the daemon owns this perch's
56	    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`
57	    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is
58	    /// the per-pid probe. The single liveness resolver
59	    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.
60	    #[serde(skip_serializing_if = "Option::is_none")]
61	    pub status: Option<String>,
62	    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this
63	    /// endpoint's activity (pulse tick / input / bind). The recency signal the
64	    /// consent gate resolves "most-recently-active live session" against
65	    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never
66	    /// stamped (an interim perch, or one with no activity yet); a resolver treats
67	    /// that as least-recent.
68	    #[serde(skip_serializing_if = "Option::is_none")]
69	    pub last_active_ms: Option<u64>,
70	    /// The node currently remote-driving this endpoint's session (hex node id),
71	    /// stamped by the daemon when it serves a net-attached surface and cleared
72	    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
73	    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
74	    /// so a pre-D5c record (no field) still parses.
75	    #[serde(default, skip_serializing_if = "Option::is_none")]
76	    pub driven_by: Option<String>,
77	    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
78	    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
79	    /// durable record (`spt_daemon::resting` owns the transitions; this field
80	    /// is only ever written through [`set_rest_state`]). Distinct from
81	    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
82	    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
83	    /// pre-D9-2 / interim record (readers fall back to liveness-derived
84	    /// status). `default` so older records still parse.
85	    #[serde(default, skip_serializing_if = "Option::is_none")]
86	    pub rest_state: Option<String>,
87	    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
88	    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
89	    /// dormancy onset, never from last activity). Present iff `rest_state`
90	    /// is `"dormant"`; cleared on every other transition.
91	    #[serde(default, skip_serializing_if = "Option::is_none")]
92	    pub dormant_since_ms: Option<u64>,
93	    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
94	    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
95	    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
96	    /// even when the node enables it.
97	    #[serde(default, skip_serializing_if = "Option::is_none")]
98	    pub auto_suspend_after_ms: Option<u64>,
99	    /// The endpoint-authored resource-advertisement blurb (D9-4,
100	    /// REQ-INST-14): the agent's own "what I can serve" line, written via
101	    /// `spt resources set` ([`set_resources`]) and advertised into every
102	    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level
103	    /// `daemon.json` seed (if any) advertises instead — config seeds the
104	    /// default, the agent refines its own at runtime (CONTEXT §resource
105	    /// advertisement, both-authored).
106	    #[serde(default, skip_serializing_if = "Option::is_none")]
107	    pub resources: Option<String>,
108	    /// The endpoint's **immutable home subnet** (D9-5, REQ-INST-15 —
109	    /// ADR-0010): assigned at creation ([`crate::home::assign_home`]) or by
110	    /// first-join adoption ([`crate::home::adopt_for_unset`]), then never
111	    /// changed — **no setter exists**; `spt fork` is the cross-subnet move.
112	    /// Absent ⇒ local-only (unpaired at creation, awaiting first join) or a
113	    /// pre-D9-5 record. Creation seams must **carry this forward** on a
114	    /// re-bind (a revive rewrites `info.json`; home survives by contract).
115	    #[serde(default, skip_serializing_if = "Option::is_none")]
116	    pub home_subnet: Option<String>,
117	    /// The adapter chosen at creation (D9-5, REQ-INST-15 — the
118	    /// adapter-at-creation rule): recorded when a creation/bind path knows
119	    /// it, changed **only** via launch/resume-under-new (the bind paths are
120	    /// the sole writers; no standalone setter). Absent ⇒ created by an
121	    /// adapterless surface (`spt ready`) or a pre-D9-5 record.
122	    #[serde(default, skip_serializing_if = "Option::is_none")]
123	    pub adapter: Option<String>,
124	}
125
126	impl InfoJson {
127	    /// Construct a numeric-pid record.
128	    pub fn new(id: &str, started: &str, pid: u32, session_id: &str, state: &str) -> InfoJson {
129	        InfoJson {
130	            id: id.to_string(),
131	            started: started.to_string(),
132	            pid: PidValue::Numeric(pid),
133	            session_id: session_id.to_string(),
134	            state: state.to_string(),

    pub id: String,
    /// Creation timestamp (opaque string; producer's format).
    pub started: String,
    /// Owning process pid, or the `"BUSY"` sentinel.
    pub pid: PidValue,
    /// Harness session id bound to this perch.
    pub session_id: String,
    /// Endpoint type tag (`spt_proto::endpoint::EndpointType::as_tag`).
    pub state: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_pid: Option<u32>,
    /// Working directory the perch was created in (forward-slash normalized).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /// Daemon-managed liveness status (`online`/`offline`) for **daemon-hosted**
    /// perches (Psyche, spt-hosted Self). Present ⇒ the daemon owns this perch's
    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`
    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is
    /// the per-pid probe. The single liveness resolver
    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this
    /// endpoint's activity (pulse tick / input / bind). The recency signal the
    /// consent gate resolves "most-recently-active live session" against
    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never
    /// stamped (an interim perch, or one with no activity yet); a resolver treats
    /// that as least-recent.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_active_ms: Option<u64>,
    /// The node currently remote-driving this endpoint's session (hex node id),
    /// stamped by the daemon when it serves a net-attached surface and cleared
    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
    /// so a pre-D5c record (no field) still parses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub driven_by: Option<String>,
    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
    /// durable record (`spt_daemon::resting` owns the transitions; this field
    /// is only ever written through [`set_rest_state`]). Distinct from
    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
    /// pre-D9-2 / interim record (readers fall back to liveness-derived
    /// status). `default` so older records still parse.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rest_state: Option<String>,
    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
    /// dormancy onset, never from last activity). Present iff `rest_state`
    /// is `"dormant"`; cleared on every other transition.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dormant_since_ms: Option<u64>,
    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auto_suspend_after_ms: Option<u64>,
    /// The endpoint-authored resource-advertisement blurb (D9-4,
    /// REQ-INST-14): the agent's own "what I can serve" line, written via
    /// `spt resources set` ([`set_resources`]) and advertised into every
    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level
    /// `daemon.json` seed (if any) advertises instead — config seeds the
    /// default, the agent refines its own at runtime (CONTEXT §resource
    /// advertisement, both-authored).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resources: Option<String>,
    /// The endpoint's **immutable home subnet** (D9-5, REQ-INST-15 —
    /// ADR-0010): assigned at creation ([`crate::home::assign_home`]) or by
    /// first-join adoption ([`crate::home::adopt_for_unset`]), then never
    /// changed — **no setter exists**; `spt fork` is the cross-subnet move.
    /// Absent ⇒ local-only (unpaired at creation, awaiting first join) or a
    /// pre-D9-5 record. Creation seams must **carry this forward** on a
    /// re-bind (a revive rewrites `info.json`; home survives by contract).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub home_subnet: Option<String>,
    /// The adapter chosen at creation (D9-5, REQ-INST-15 — the
    /// adapter-at-creation rule): recorded when a creation/bind path knows
    /// it, changed **only** via launch/resume-under-new (the bind paths are
    /// the sole writers; no standalone setter). Absent ⇒ created by an
    /// adapterless surface (`spt ready`) or a pre-D9-5 record.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub adapter: Option<String>,
}

impl InfoJson {
    /// Construct a numeric-pid record.
    pub fn new(id: &str, started: &str, pid: u32, session_id: &str, state: &str) -> InfoJson {
        InfoJson {
            id: id.to_string(),
            started: started.to_string(),
            pid: PidValue::Numeric(pid),
            session_id: session_id.to_string(),
            state: state.to_string(),


43:pub struct ShellInfo {
47:    pub owner: String,
61:    pub fn new(owner: &str, adapter_name: &str) -> Self {
102:pub fn read_shell_info(shell_perch: &Path) -> Option<ShellInfo> {
110:pub fn write_shell_info(shell_perch: &Path, info: &ShellInfo) -> std::io::Result<()> {
130:pub fn mint_shell_id(owlery: &Path, owner: &str, adapter: &str) -> String {
157:pub fn list_shells(owlery: &Path, owner: &str) -> Vec<(String, ShellInfo)> {
179:pub fn resolve_shell_ref(
205:pub fn spawn_record(
230:pub fn rename_shell(
253:pub fn teardown_shell(owlery: &Path, owner: &str, shell_ref: &str) -> Result<String, ShellError> {


Edit

    /// List this node's perches (id, state, address, ready, alive).
    List,
    /// Soft-stop a perch (spool preserved).
    ///
    /// Removes the ready marker and unregisters the perch; the spool is
    /// preserved.
    Stop {
        /// Perch id to stop.
        id: String,
    },
    /// Print this session's own perch id.


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)

    /// List this node's perches (id, state, address, ready, alive).
    List,
    /// Soft-stop a perch (spool preserved).
    ///
    /// Removes the ready marker and unregisters the perch; the spool is
    /// preserved.
    Stop {
        /// Perch id to stop.
        id: String,
    },
    /// Print this session's own perch id.

-    /// List this node's perches (id, state, address, ready, alive).

-    /// Soft-stop a perch (spool preserved).

-    /// Removes the ready marker and unregisters the perch; the spool is

/// Resolved from $OWL_SESSION_ID / $SPT_AGENT_ID.


    /// Show a hosted session's live activity buffer (PTY digest).
    ///
    /// The at-a-glance "what is this agent doing now" view. Pulls a
    /// snapshot, or `--follow`s the delta-stream. Local endpoints only.
    Digest {
        /// The (local) endpoint id to read.
        id: String,
        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).
        #[arg(long)]
        follow: bool,
        /// Emit structured JSON instead of the human-glanceable render.
        #[arg(long)]
        json: bool,
    },
    /// Rename an endpoint's logical id across its on-disk state.
    ///
    /// Rippled everywhere the id appears: the endpoint's perch dir, its
    /// nested companion/worker perches, and every record naming it.
    /// Refuses while the perch is live (stop it first).
    Rename {
        /// The endpoint's current (bare) id.
        old_id: String,
        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
        new_id: String,
    },
    /// Endpoint access whitelist for unsolicited off-node inbound.
    ///
    /// Controls which origin nodes may send an endpoint unsolicited
    /// off-node inbound. Absent entry = open; `allow` flips the endpoint
    /// to restricted; revoking the last node leaves it locked down;
    /// `open` deletes the restriction.
    Access {
        #[command(subcommand)]
        action: AccessCmd,
    },
    /// Consent grant store: gated capabilities held on this node.

    /// Endpoint operations: list, lifecycle, fork, digest, access, description.
    ///
    /// The noun home for per-endpoint verbs (M8 decision 1). Bare
    /// `spt endpoint` renders the merged listing — every member subnet's
    /// endpoints grouped by subnet, this session's own endpoint pinned
    /// distinctly at the top.
    Endpoint {
        #[command(subcommand)]
        action: Option<EndpointCmd>,
    },
    /// Consent grant store: gated capabilities held on this node.


    /// Show a hosted session's live activity buffer (PTY digest).
    ///
    /// The at-a-glance "what is this agent doing now" view. Pulls a
    /// snapshot, or `--follow`s the delta-stream. Local endpoints only.
    Digest {
        /// The (local) endpoint id to read.
        id: String,
        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).
        #[arg(long)]
        follow: bool,
        /// Emit structured JSON instead of the human-glanceable render.
        #[arg(long)]
        json: bool,
    },
    /// Rename an endpoint's logical id across its on-disk state.
    ///
    /// Rippled everywhere the id appears: the endpoint's perch dir, its
    /// nested companion/worker perches, and every record naming it.
    /// Refuses while the perch is live (stop it first).
    Rename {
        /// The endpoint's current (bare) id.
        old_id: String,
        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
        new_id: String,
    },
    /// Endpoint access whitelist for unsolicited off-node inbound.
    ///
    /// Controls which origin nodes may send an endpoint unsolicited
    /// off-node inbound. Absent entry = open; `allow` flips the endpoint
    /// to restricted; revoking the last node leaves it locked down;
    /// `open` deletes the restriction.
    Access {
        #[command(subcommand)]
        action: AccessCmd,
    },
    /// Consent grant store: gated capabilities held on this node.

    /// Endpoint operations: list, lifecycle, fork, digest, access, description.
    ///
    /// The noun home for per-endpoint verbs (M8 decision 1). Bare
    /// `spt endpoint` renders the merged listing — every member subnet's
    /// endpoints grouped by subnet, this session's own endpoint pinned
    /// distinctly at the top.
    Endpoint {
        #[command(subcommand)]
        action: Option<EndpointCmd>,
    },
    /// Consent grant store: gated capabilities held on this node.

/// The topic to print (omit to list available topics).

-    /// Show a hosted session's live activity buffer (PTY digest).

+    /// Endpoint operations: list, lifecycle, fork, digest, access, description.

-    /// The at-a-glance "what is this agent doing now" view. Pulls a

-    /// snapshot, or `--follow`s the delta-stream. Local endpoints only.

-        /// The (local) endpoint id to read.

-        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).

-        /// Emit structured JSON instead of the human-glanceable render.

-    /// Rename an endpoint's logical id across its on-disk state.

-    /// Rippled everywhere the id appears: the endpoint's perch dir, its

-    /// nested companion/worker perches, and every record naming it.

-    /// Refuses while the perch is live (stop it first).

-        /// The endpoint's current (bare) id.

-        /// The new (bare) id — charset-validated; `:`/`@` are reserved.

-    /// Endpoint access whitelist for unsolicited off-node inbound.

-    /// Controls which origin nodes may send an endpoint unsolicited

-    /// off-node inbound. Absent entry = open; `allow` flips the endpoint

-    /// to restricted; revoking the last node leaves it locked down;

-    /// `open` deletes the restriction.

+    /// The noun home for per-endpoint verbs (M8 decision 1). Bare

+    /// `spt endpoint` renders the merged listing — every member subnet's

+    /// endpoints grouped by subnet, this session's own endpoint pinned


D3 — install & elevation hardening

D5 — docs + CI + acceptance prep


    /// Issue a subnet-wide user notification.
    ///
    /// Produced into the replicated notification spool and first-fired at
    /// the user's most-recently-active endpoint in that subnet. Body from
    /// the trailing arg, or stdin when omitted.
    Notify {
        /// Notification body (read from stdin when omitted).
        body: Option<String>,
        /// Which subnet to notify. Required only when the node holds several
        /// (never guessed).
        #[arg(long)]
        subnet: Option<String>,
        /// Issuer endpoint id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
    },
    /// Inspect and acknowledge notifications.

/// Inspect and acknowledge notifications.


    /// Issue a subnet-wide user notification.
    ///
    /// Produced into the replicated notification spool and first-fired at
    /// the user's most-recently-active endpoint in that subnet. Body from
    /// the trailing arg, or stdin when omitted.
    Notify {
        /// Notification body (read from stdin when omitted).
        body: Option<String>,
        /// Which subnet to notify. Required only when the node holds several
        /// (never guessed).
        #[arg(long)]
        subnet: Option<String>,
        /// Issuer endpoint id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
    },
    /// Inspect and acknowledge notifications.

-    /// Issue a subnet-wide user notification.

-    /// Produced into the replicated notification spool and first-fired at

-    /// the user's most-recently-active endpoint in that subnet. Body from

-    /// the trailing arg, or stdin when omitted.

-        /// Notification body (read from stdin when omitted).

-        /// Which subnet to notify. Required only when the node holds several

-        /// Issuer endpoint id (auto-detected from session if omitted).

/// Dismissal is the explicit ack — it latches and replicates


    /// Fork an endpoint into another subnet as a NEW identity.
    ///
    /// Home subnets are immutable — fork is the cross-subnet move, never
    /// a re-home. Seeds the fork with a one-time copy of the source's
    /// mind (live + project tiers); the two diverge immediately (no
    /// ongoing sync). The source is untouched unless --delete-source.
    /// Same-node only today (one node holds one perch per name, so a
    /// local fork needs a new id).
    Fork {
        /// The source endpoint (must exist on this node).
        src: String,
        /// The fork's id (must differ from the source on the same node).
        new_id: String,
        /// The fork's home subnet — the target (must be a member).
        #[arg(long)]
        subnet: String,
        /// Delete the source endpoint (perch + tracked mind) after the copy.
        #[arg(long)]
        delete_source: bool,
    },
    /// The subnet resource registry — an agent yellow-pages.
    ///
    /// Per-endpoint free-text service blurbs. `set` authors your own,
    /// `show` reads a local endpoint's, `list` projects the registry view
    /// over visible rows.
    Resources {
        #[command(subcommand)]
        action: ResourcesCmd,
    },
    /// Rest an endpoint cold (the suspend edge).
    ///
    /// The resting state machine's suspend edge. From dormant — or
    /// straight from active, in which case the final
    /// context save still fires first. Accepts a qualified `id@node` to
    /// suspend an instance on a paired peer.
    Suspend {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Wake a resting endpoint in place.
    ///
    /// Re-activates the existing seat (state's already there — no fresh
    /// spawn), resurfaces undismissed notifications, and requests an
    /// immediate context freshness pull from trusted peers. Accepts a
    /// qualified `id@node` for an instance on a paired peer.
    Wake {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Gracefully shut down an agent's own endpoint.
    ///
    /// Soft-stops the listener, then the suspend edge — the final context
    /// save fires and persistent shells cascade offline with it.
    Shutdown {
        /// The endpoint id (defaults to the session's own perch).
        id: Option<String>,
    },
    /// Harness-contract inbound surface (hook entry points).
    ///
    /// The entry points a harness's hooks fire to keep spt-core's on-disk
    /// state in sync.
    Api(crate::api::ApiArgs),
    /// Run the per-machine daemon. Internal: any `spt` invocation that needs
    /// it auto-starts it; operators rarely run it directly.
    #[command(hide = true)]
    Daemon,
}

    /// Harness-contract inbound surface (hook entry points).
    ///
    /// The entry points a harness's hooks fire to keep spt-core's on-disk
    /// state in sync.
    Api(crate::api::ApiArgs),
    /// The per-machine daemon: run, stop, or read node status.
    ///
    /// Bare `spt daemon` renders the node status view — daemon state,
    /// member subnets, local endpoints (M8 decision 25).
    Daemon {
        #[command(subcommand)]
        action: Option<DaemonCmd>,
    },
}

/// `spt endpoint <action>` subcommands (M8 decisions 1–2, 25 — REQ-CLI-1).
/// The verbs ride their pre-M8 arg shapes verbatim; only the noun home is
/// new (decision 9: breaking renames land clean, no shims).
// [impl->REQ-CLI-1]
#[derive(Subcommand)]
enum EndpointCmd {
    /// Merged endpoint listing (the bare `spt endpoint` view).
    ///
    /// Default: every member subnet's endpoints grouped by subnet, with
    /// this session's own endpoint pinned at the top. `--local` lists this
    /// node's perches instead (id, state, address, ready, alive);
    /// `--subnet` filters to one subnet; `--detail` adds each endpoint's
    /// description blurb (the resource-registry yellow-pages projection).
    List {
        /// List this node's local perches instead of the subnet view.
        #[arg(long)]
        local: bool,
        /// Limit the subnet view to one subnet.
        #[arg(long, conflicts_with = "local")]
        subnet: Option<String>,
        /// Add each endpoint's description blurb to the rows.
        #[arg(long, conflicts_with = "local")]
        detail: bool,
    },
    /// Fork an endpoint into another subnet as a NEW identity.
    ///
    /// Home subnets are immutable — fork is the cross-subnet move, never
    /// a re-home. Seeds the fork with a one-time copy of the source's
    /// mind (live + project tiers); the two diverge immediately (no
    /// ongoing sync). The source is untouched unless --delete-source.
    /// Same-node only today (one node holds one perch per name, so a
    /// local fork needs a new id).
    Fork {
        /// The source endpoint (must exist on this node).
        src: String,
        /// The fork's id (must differ from the source on the same node).
        new_id: String,
        /// The fork's home subnet — the target (must be a member).
        #[arg(long)]
        subnet: String,
        /// Delete the source endpoint (perch + tracked mind) after the copy.
        #[arg(long)]
        delete_source: bool,
    },
    /// Rest an endpoint cold (the suspend edge).
    ///
    /// The resting state machine's suspend edge. From dormant — or
    /// straight from active, in which case the final
    /// context save still fires first. Accepts a qualified `id@node` to
    /// suspend an instance on a paired peer.
    Suspend {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Wake a resting endpoint in place.
    ///
    /// Re-activates the existing seat (state's already there — no fresh
    /// spawn), resurfaces undismissed notifications, and requests an
    /// immediate context freshness pull from trusted peers. Accepts a
    /// qualified `id@node` for an instance on a paired peer.
    Wake {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Gracefully shut down an agent's own endpoint.
    ///
    /// Soft-stops the listener, then the suspend edge — the final context
    /// save fires and persistent shells cascade offline with it.
    Shutdown {
        /// The endpoint id (defaults to the session's own perch).
        id: Option<String>,
    },
    /// Soft-stop a perch (spool preserved).
    ///
    /// Removes the ready marker and unregisters the perch; the spool is
    /// preserved.
    Stop {
        /// Perch id to stop.
        id: String,
    },
    /// Rename an endpoint's logical id across its on-disk state.
    ///
    /// Rippled everywhere the id appears: the endpoint's perch dir, its
    /// nested companion/worker perches, and every record naming it.
    /// Refuses while the perch is live (stop it first).
    Rename {
        /// The endpoint's current (bare) id.
        old_id: String,
        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
        new_id: String,
    },
    /// Show a hosted session's live activity buffer (PTY digest).
    ///
    /// The at-a-glance "what is this agent doing now" view. Pulls a
    /// snapshot, or `--follow`s the delta-stream. Local endpoints only.
    Digest {
        /// The (local) endpoint id to read.
        id: String,
        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).
        #[arg(long)]
        follow: bool,
        /// Emit structured JSON instead of the human-glanceable render.
        #[arg(long)]
        json: bool,
    },
    /// Endpoint access whitelist for unsolicited off-node inbound.
    ///
    /// Controls which origin nodes may send an endpoint unsolicited
    /// off-node inbound. Absent entry = open; `allow` flips the endpoint
    /// to restricted; revoking the last node leaves it locked down;
    /// `open` deletes the restriction.
    Access {
        #[command(subcommand)]
        action: AccessCmd,
    },
    /// The endpoint's service-description blurb (ex-`resources`).
    ///
    /// Bare `description` shows your own; `set` authors it. The cross-node
    /// projection over every visible endpoint is `endpoint list --detail`.
    Description {
        #[command(subcommand)]
        action: Option<DescriptionCmd>,
    },
}

/// `spt endpoint description <action>` subcommands (M8 decision 25c — the
/// ex-`resources` set/show pair; the list projection moved to
/// `endpoint list --detail`).
#[derive(Subcommand)]
enum DescriptionCmd {
    /// Author this endpoint's blurb (the agent refines its own at runtime;
    /// an empty string clears it back to the node-config seed).
    Set {
        /// The blurb text ("" clears).
        text: String,
        /// Which local endpoint (auto-detected from the session if omitted).
        #[arg(long)]
        id: Option<String>,
    },
    /// Show a local endpoint's authored blurb (the bare `description` view).
    Show {
        /// The local endpoint id (auto-detected if omitted).
        id: Option<String>,
    },
}

/// `spt daemon <action>` subcommands (M8 decisions 5, 23, 25b — REQ-CLI-2).
// [impl->REQ-CLI-2]
#[derive(Subcommand)]
enum DaemonCmd {
    /// Run the per-machine daemon in the foreground (the auto-start entry
    /// point — any `spt` invocation that needs the daemon starts this
    /// detached; operators rarely run it directly).
    Run,
    /// Stop the running daemon (graceful: acks, then exits).
    Stop,
    /// Node status: daemon state, member subnets, local endpoints (the
    /// bare `spt daemon` view).
    Status,
}


    /// Fork an endpoint into another subnet as a NEW identity.
    ///
    /// Home subnets are immutable — fork is the cross-subnet move, never
    /// a re-home. Seeds the fork with a one-time copy of the source's
    /// mind (live + project tiers); the two diverge immediately (no
    /// ongoing sync). The source is untouched unless --delete-source.
    /// Same-node only today (one node holds one perch per name, so a
    /// local fork needs a new id).
    Fork {
        /// The source endpoint (must exist on this node).
        src: String,
        /// The fork's id (must differ from the source on the same node).
        new_id: String,
        /// The fork's home subnet — the target (must be a member).
        #[arg(long)]
        subnet: String,
        /// Delete the source endpoint (perch + tracked mind) after the copy.
        #[arg(long)]
        delete_source: bool,
    },
    /// The subnet resource registry — an agent yellow-pages.
    ///
    /// Per-endpoint free-text service blurbs. `set` authors your own,
    /// `show` reads a local endpoint's, `list` projects the registry view
    /// over visible rows.
    Resources {
        #[command(subcommand)]
        action: ResourcesCmd,
    },
    /// Rest an endpoint cold (the suspend edge).
    ///
    /// The resting state machine's suspend edge. From dormant — or
    /// straight from active, in which case the final
    /// context save still fires first. Accepts a qualified `id@node` to
    /// suspend an instance on a paired peer.
    Suspend {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Wake a resting endpoint in place.
    ///
    /// Re-activates the existing seat (state's already there — no fresh
    /// spawn), resurfaces undismissed notifications, and requests an
    /// immediate context freshness pull from trusted peers. Accepts a
    /// qualified `id@node` for an instance on a paired peer.
    Wake {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Gracefully shut down an agent's own endpoint.
    ///
    /// Soft-stops the listener, then the suspend edge — the final context
    /// save fires and persistent shells cascade offline with it.
    Shutdown {
        /// The endpoint id (defaults to the session's own perch).
        id: Option<String>,
    },
    /// Harness-contract inbound surface (hook entry points).
    ///
    /// The entry points a harness's hooks fire to keep spt-core's on-disk
    /// state in sync.
    Api(crate::api::ApiArgs),
    /// Run the per-machine daemon. Internal: any `spt` invocation that needs
    /// it auto-starts it; operators rarely run it directly.
    #[command(hide = true)]
    Daemon,
}

    /// Harness-contract inbound surface (hook entry points).
    ///
    /// The entry points a harness's hooks fire to keep spt-core's on-disk
    /// state in sync.
    Api(crate::api::ApiArgs),
    /// The per-machine daemon: run, stop, or read node status.
    ///
    /// Bare `spt daemon` renders the node status view — daemon state,
    /// member subnets, local endpoints (M8 decision 25).
    Daemon {
        #[command(subcommand)]
        action: Option<DaemonCmd>,
    },
}

/// `spt endpoint <action>` subcommands (M8 decisions 1–2, 25 — REQ-CLI-1).
/// The verbs ride their pre-M8 arg shapes verbatim; only the noun home is
/// new (decision 9: breaking renames land clean, no shims).
// [impl->REQ-CLI-1]
#[derive(Subcommand)]
enum EndpointCmd {
    /// Merged endpoint listing (the bare `spt endpoint` view).
    ///
    /// Default: every member subnet's endpoints grouped by subnet, with
    /// this session's own endpoint pinned at the top. `--local` lists this
    /// node's perches instead (id, state, address, ready, alive);
    /// `--subnet` filters to one subnet; `--detail` adds each endpoint's
    /// description blurb (the resource-registry yellow-pages projection).
    List {
        /// List this node's local perches instead of the subnet view.
        #[arg(long)]
        local: bool,
        /// Limit the subnet view to one subnet.
        #[arg(long, conflicts_with = "local")]
        subnet: Option<String>,
        /// Add each endpoint's description blurb to the rows.
        #[arg(long, conflicts_with = "local")]
        detail: bool,
    },
    /// Fork an endpoint into another subnet as a NEW identity.
    ///
    /// Home subnets are immutable — fork is the cross-subnet move, never
    /// a re-home. Seeds the fork with a one-time copy of the source's
    /// mind (live + project tiers); the two diverge immediately (no
    /// ongoing sync). The source is untouched unless --delete-source.
    /// Same-node only today (one node holds one perch per name, so a
    /// local fork needs a new id).
    Fork {
        /// The source endpoint (must exist on this node).
        src: String,
        /// The fork's id (must differ from the source on the same node).
        new_id: String,
        /// The fork's home subnet — the target (must be a member).
        #[arg(long)]
        subnet: String,
        /// Delete the source endpoint (perch + tracked mind) after the copy.
        #[arg(long)]
        delete_source: bool,
    },
    /// Rest an endpoint cold (the suspend edge).
    ///
    /// The resting state machine's suspend edge. From dormant — or
    /// straight from active, in which case the final
    /// context save still fires first. Accepts a qualified `id@node` to
    /// suspend an instance on a paired peer.
    Suspend {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Wake a resting endpoint in place.
    ///
    /// Re-activates the existing seat (state's already there — no fresh
    /// spawn), resurfaces undismissed notifications, and requests an
    /// immediate context freshness pull from trusted peers. Accepts a
    /// qualified `id@node` for an instance on a paired peer.
    Wake {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Gracefully shut down an agent's own endpoint.
    ///
    /// Soft-stops the listener, then the suspend edge — the final context
    /// save fires and persistent shells cascade offline with it.
    Shutdown {
        /// The endpoint id (defaults to the session's own perch).
        id: Option<String>,
    },
    /// Soft-stop a perch (spool preserved).
    ///
    /// Removes the ready marker and unregisters the perch; the spool is
    /// preserved.
    Stop {
        /// Perch id to stop.
        id: String,
    },
    /// Rename an endpoint's logical id across its on-disk state.
    ///
    /// Rippled everywhere the id appears: the endpoint's perch dir, its
    /// nested companion/worker perches, and every record naming it.
    /// Refuses while the perch is live (stop it first).
    Rename {
        /// The endpoint's current (bare) id.
        old_id: String,
        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
        new_id: String,
    },
    /// Show a hosted session's live activity buffer (PTY digest).
    ///
    /// The at-a-glance "what is this agent doing now" view. Pulls a
    /// snapshot, or `--follow`s the delta-stream. Local endpoints only.
    Digest {
        /// The (local) endpoint id to read.
        id: String,
        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).
        #[arg(long)]
        follow: bool,
        /// Emit structured JSON instead of the human-glanceable render.
        #[arg(long)]
        json: bool,
    },
    /// Endpoint access whitelist for unsolicited off-node inbound.
    ///
    /// Controls which origin nodes may send an endpoint unsolicited
    /// off-node inbound. Absent entry = open; `allow` flips the endpoint
    /// to restricted; revoking the last node leaves it locked down;
    /// `open` deletes the restriction.
    Access {
        #[command(subcommand)]
        action: AccessCmd,
    },
    /// The endpoint's service-description blurb (ex-`resources`).
    ///
    /// Bare `description` shows your own; `set` authors it. The cross-node
    /// projection over every visible endpoint is `endpoint list --detail`.
    Description {
        #[command(subcommand)]
        action: Option<DescriptionCmd>,
    },
}

/// `spt endpoint description <action>` subcommands (M8 decision 25c — the
/// ex-`resources` set/show pair; the list projection moved to
/// `endpoint list --detail`).
#[derive(Subcommand)]
enum DescriptionCmd {
    /// Author this endpoint's blurb (the agent refines its own at runtime;
    /// an empty string clears it back to the node-config seed).
    Set {
        /// The blurb text ("" clears).
        text: String,
        /// Which local endpoint (auto-detected from the session if omitted).
        #[arg(long)]
        id: Option<String>,
    },
    /// Show a local endpoint's authored blurb (the bare `description` view).
    Show {
        /// The local endpoint id (auto-detected if omitted).
        id: Option<String>,
    },
}

/// `spt daemon <action>` subcommands (M8 decisions 5, 23, 25b — REQ-CLI-2).
// [impl->REQ-CLI-2]
#[derive(Subcommand)]
enum DaemonCmd {
    /// Run the per-machine daemon in the foreground (the auto-start entry
    /// point — any `spt` invocation that needs the daemon starts this
    /// detached; operators rarely run it directly).
    Run,
    /// Stop the running daemon (graceful: acks, then exits).
    Stop,
    /// Node status: daemon state, member subnets, local endpoints (the
    /// bare `spt daemon` view).
    Status,
}

+    /// Harness-contract inbound surface (hook entry points).

+    /// The entry points a harness's hooks fire to keep spt-core's on-disk

+    /// The per-machine daemon: run, stop, or read node status.

+    /// Bare `spt daemon` renders the node status view — daemon state,

+    /// member subnets, local endpoints (M8 decision 25).

+/// `spt endpoint <action>` subcommands (M8 decisions 1–2, 25 — REQ-CLI-1).

+/// The verbs ride their pre-M8 arg shapes verbatim; only the noun home is

+/// new (decision 9: breaking renames land clean, no shims).

+    /// Merged endpoint listing (the bare `spt endpoint` view).

+    /// Default: every member subnet's endpoints grouped by subnet, with

+    /// this session's own endpoint pinned at the top. `--local` lists this

+    /// node's perches instead (id, state, address, ready, alive);

+    /// `--subnet` filters to one subnet; `--detail` adds each endpoint's

+    /// description blurb (the resource-registry yellow-pages projection).

+        /// List this node's local perches instead of the subnet view.

+        /// Limit the subnet view to one subnet.

+        #[arg(long, conflicts_with = "local")]

+        /// Add each endpoint's description blurb to the rows.

-    /// The subnet resource registry — an agent yellow-pages.

-    /// Per-endpoint free-text service blurbs. `set` authors your own,

-    /// `show` reads a local endpoint's, `list` projects the registry view

-    /// Harness-contract inbound surface (hook entry points).

+    /// Soft-stop a perch (spool preserved).

-    /// The entry points a harness's hooks fire to keep spt-core's on-disk

-    /// Run the per-machine daemon. Internal: any `spt` invocation that needs

-    /// it auto-starts it; operators rarely run it directly.

+    /// Removes the ready marker and unregisters the perch; the spool is

+    /// Rename an endpoint's logical id across its on-disk state.

+    /// Rippled everywhere the id appears: the endpoint's perch dir, its

+    /// nested companion/worker perches, and every record naming it.

+    /// Refuses while the perch is live (stop it first).

+        /// The endpoint's current (bare) id.

+        /// The new (bare) id — charset-validated; `:`/`@` are reserved.

+    /// Show a hosted session's live activity buffer (PTY digest).

+    /// The at-a-glance "what is this agent doing now" view. Pulls a

+    /// snapshot, or `--follow`s the delta-stream. Local endpoints only.

+        /// The (local) endpoint id to read.

+        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).

+        /// Emit structured JSON instead of the human-glanceable render.

+    /// Endpoint access whitelist for unsolicited off-node inbound.

+    /// Controls which origin nodes may send an endpoint unsolicited

+    /// off-node inbound. Absent entry = open; `allow` flips the endpoint

+    /// to restricted; revoking the last node leaves it locked down;

+    /// `open` deletes the restriction.

+    /// The endpoint's service-description blurb (ex-`resources`).

+    /// Bare `description` shows your own; `set` authors it. The cross-node

+    /// projection over every visible endpoint is `endpoint list --detail`.

+        action: Option<DescriptionCmd>,

+/// `spt endpoint description <action>` subcommands (M8 decision 25c — the

+/// ex-`resources` set/show pair; the list projection moved to

+    /// Author this endpoint's blurb (the agent refines its own at runtime;

+    /// an empty string clears it back to the node-config seed).

+        /// The blurb text ("" clears).

+        /// Which local endpoint (auto-detected from the session if omitted).

+    /// Show a local endpoint's authored blurb (the bare `description` view).

+        /// The local endpoint id (auto-detected if omitted).

+/// `spt daemon <action>` subcommands (M8 decisions 5, 23, 25b — REQ-CLI-2).

+    /// Run the per-machine daemon in the foreground (the auto-start entry

+    /// point — any `spt` invocation that needs the daemon starts this

+    /// detached; operators rarely run it directly).

+    /// Stop the running daemon (graceful: acks, then exits).

+    /// Node status: daemon state, member subnets, local endpoints (the

/// `spt access <action>` subcommands (REQ-SEC-1).


/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).
#[derive(Subcommand)]
enum ResourcesCmd {
    /// Author this endpoint's blurb (the agent refines its own at runtime;
    /// an empty string clears it back to the node-config seed).
    Set {
        /// The blurb text ("" clears).
        text: String,
        /// Which local endpoint (auto-detected from the session if omitted).
        #[arg(long)]
        id: Option<String>,
    },
    /// Show a local endpoint's authored blurb.
    Show {
        /// The local endpoint id (auto-detected if omitted).
        id: Option<String>,
    },
    /// The subnet resource-registry projection: `(id, node, status, blurb)`
    /// over VISIBLE rows only (hidden endpoints never appear — discovery
    /// leaks nothing a viewer couldn't reach).
    List {
        /// Limit to one subnet (all member subnets otherwise).
        #[arg(long)]
        subnet: Option<String>,
    },
}

/// `spt notif <action>` subcommands (ADR-0007).

/// `spt notif <action>` subcommands (ADR-0007).


/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).
#[derive(Subcommand)]
enum ResourcesCmd {
    /// Author this endpoint's blurb (the agent refines its own at runtime;
    /// an empty string clears it back to the node-config seed).
    Set {
        /// The blurb text ("" clears).
        text: String,
        /// Which local endpoint (auto-detected from the session if omitted).
        #[arg(long)]
        id: Option<String>,
    },
    /// Show a local endpoint's authored blurb.
    Show {
        /// The local endpoint id (auto-detected if omitted).
        id: Option<String>,
    },
    /// The subnet resource-registry projection: `(id, node, status, blurb)`
    /// over VISIBLE rows only (hidden endpoints never appear — discovery
    /// leaks nothing a viewer couldn't reach).
    List {
        /// Limit to one subnet (all member subnets otherwise).
        #[arg(long)]
        subnet: Option<String>,
    },
}

/// `spt notif <action>` subcommands (ADR-0007).

-/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).

-    /// Author this endpoint's blurb (the agent refines its own at runtime;

-    /// an empty string clears it back to the node-config seed).

-        /// The blurb text ("" clears).

-        /// Which local endpoint (auto-detected from the session if omitted).

-    /// Show a local endpoint's authored blurb.

-        /// The local endpoint id (auto-detected if omitted).

-    /// The subnet resource-registry projection: `(id, node, status, blurb)`

-    /// over VISIBLE rows only (hidden endpoints never appear — discovery

-    /// leaks nothing a viewer couldn't reach).

-        /// Limit to one subnet (all member subnets otherwise).


    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
    },
}

    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
    },
    /// Issue a subnet-wide user notification.
    ///
    /// Produced into the replicated notification spool and first-fired at
    /// the user's most-recently-active endpoint in that subnet. Body from
    /// the trailing arg, or stdin when omitted. Targets the calling
    /// endpoint's HOME subnet unless `--target` names another (M8
    /// decision 25: no resolvable home + no `--target` = refuse).
    Notify {
        /// Notification body (read from stdin when omitted).
        body: Option<String>,
        /// Target subnet (defaults to the calling endpoint's home subnet).
        #[arg(long)]
        target: Option<String>,
        /// Issuer endpoint id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
    },
}


    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
    },
}

    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
    },
    /// Issue a subnet-wide user notification.
    ///
    /// Produced into the replicated notification spool and first-fired at
    /// the user's most-recently-active endpoint in that subnet. Body from
    /// the trailing arg, or stdin when omitted. Targets the calling
    /// endpoint's HOME subnet unless `--target` names another (M8
    /// decision 25: no resolvable home + no `--target` = refuse).
    Notify {
        /// Notification body (read from stdin when omitted).
        body: Option<String>,
        /// Target subnet (defaults to the calling endpoint's home subnet).
        #[arg(long)]
        target: Option<String>,
        /// Issuer endpoint id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
    },
}

+    /// Issue a subnet-wide user notification.

+    /// Produced into the replicated notification spool and first-fired at

+    /// the user's most-recently-active endpoint in that subnet. Body from

+    /// the trailing arg, or stdin when omitted. Targets the calling

+    /// endpoint's HOME subnet unless `--target` names another (M8

+    /// decision 25: no resolvable home + no `--target` = refuse).

+        /// Notification body (read from stdin when omitted).

+        /// Target subnet (defaults to the calling endpoint's home subnet).

+        /// Issuer endpoint id (auto-detected from session if omitted).

/// Top-level commands an agent is meant to run from its own session; every


Now AGENT_COMMANDS + dispatch.


const AGENT_COMMANDS: &[&str] = &[
    "api",
    "how-to",
    "notify",
    "ready",
    "resources",
    "ring",
    "send",
    "shell",
    "shutdown",
    "whoami",
];

const AGENT_COMMANDS: &[&str] = &[
    "api",
    "endpoint",
    "how-to",
    "ready",
    "ring",
    "send",
    "shell",
    "whoami",
];


const AGENT_COMMANDS: &[&str] = &[
    "api",
    "how-to",
    "notify",
    "ready",
    "resources",
    "ring",
    "send",
    "shell",
    "shutdown",
    "whoami",
];

const AGENT_COMMANDS: &[&str] = &[
    "api",
    "endpoint",
    "how-to",
    "ready",
    "ring",
    "send",
    "shell",
    "whoami",
];

/// headings); `help_groups_cover_every_command` keeps this set honest.


        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
        Cmd::List => cmd_list(),
        Cmd::Stop { id } => cmd_stop(&id),
        Cmd::Whoami => cmd_whoami(),
        Cmd::Subnet { action } => {
            match action.unwrap_or(SubnetCmd::Status {
                name: None,
                nodes: false,
            }) {
                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),
            }
        }
        Cmd::HowTo { topic } => cmd_how_to(topic.as_deref()),
        Cmd::Access { action } => cmd_access(action),
        Cmd::Grant { action } => cmd_grant(action),
        Cmd::Adapter { action } => cmd_adapter(action),
        Cmd::Shell { action } => cmd_shell(action),
        Cmd::Notify { body, subnet, from } => cmd_notify(body, subnet, from),
        Cmd::Notif { action } => match action {
            NotifCmd::List { subnet } => cmd_notif_list(subnet),
            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),
        },
        Cmd::Update { action } => match action {
            UpdateCmd::Apply => cmd_update_apply(),
        },
        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
        Cmd::Fork {
            src,
            new_id,
            subnet,
            delete_source,
        } => cmd_fork(&src, &new_id, &subnet, delete_source),
        Cmd::Resources { action } => cmd_resources(action),
        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
        Cmd::Shutdown { id } => cmd_shutdown(id),
        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),
        Cmd::Api(args) => crate::api::run(args),
        Cmd::Daemon => cmd_daemon(),
    }
}

        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
        Cmd::Whoami => cmd_whoami(),
        Cmd::Subnet { action } => {
            match action.unwrap_or(SubnetCmd::Status {
                name: None,
                nodes: false,
            }) {
                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),
                SubnetCmd::Notify { body, target, from } => cmd_notify(body, target, from),
            }
        }
        Cmd::HowTo { topic } => cmd_how_to(topic.as_deref()),
        // The endpoint noun (M8 decisions 1–2): bare = the merged list.
        // [impl->REQ-CLI-1]
        Cmd::Endpoint { action } => match action.unwrap_or(EndpointCmd::List {
            local: false,
            subnet: None,
            detail: false,
        }) {
            EndpointCmd::List {
                local,
                subnet,
                detail,
            } => cmd_endpoint_list(local, subnet, detail),
            EndpointCmd::Fork {
                src,
                new_id,
                subnet,
                delete_source,
            } => cmd_fork(&src, &new_id, &subnet, delete_source),
            EndpointCmd::Suspend { id } => {
                cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED")
            }
            EndpointCmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
            EndpointCmd::Shutdown { id } => cmd_shutdown(id),
            EndpointCmd::Stop { id } => cmd_stop(&id),
            EndpointCmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),
            EndpointCmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
            EndpointCmd::Access { action } => cmd_access(action),
            EndpointCmd::Description { action } => {
                cmd_description(action.unwrap_or(DescriptionCmd::Show { id: None }))
            }
        },
        Cmd::Grant { action } => cmd_grant(action),
        Cmd::Adapter { action } => cmd_adapter(action),
        Cmd::Shell { action } => cmd_shell(action),
        Cmd::Notif { action } => match action {
            NotifCmd::List { subnet } => cmd_notif_list(subnet),
            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),
        },
        Cmd::Update { action } => match action {
            UpdateCmd::Apply => cmd_update_apply(),
        },
        Cmd::Api(args) => crate::api::run(args),
        // The daemon noun (M8 decisions 5, 25b): bare = the node status view.
        // [impl->REQ-CLI-2]
        Cmd::Daemon { action } => match action.unwrap_or(DaemonCmd::Status) {
            DaemonCmd::Run => cmd_daemon_run(),
            DaemonCmd::Stop => cmd_daemon_stop(),
            DaemonCmd::Status => cmd_daemon_status(),
        },
    }
}


        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
        Cmd::List => cmd_list(),
        Cmd::Stop { id } => cmd_stop(&id),
        Cmd::Whoami => cmd_whoami(),
        Cmd::Subnet { action } => {
            match action.unwrap_or(SubnetCmd::Status {
                name: None,
                nodes: false,
            }) {
                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),
            }
        }
        Cmd::HowTo { topic } => cmd_how_to(topic.as_deref()),
        Cmd::Access { action } => cmd_access(action),
        Cmd::Grant { action } => cmd_grant(action),
        Cmd::Adapter { action } => cmd_adapter(action),
        Cmd::Shell { action } => cmd_shell(action),
        Cmd::Notify { body, subnet, from } => cmd_notify(body, subnet, from),
        Cmd::Notif { action } => match action {
            NotifCmd::List { subnet } => cmd_notif_list(subnet),
            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),
        },
        Cmd::Update { action } => match action {
            UpdateCmd::Apply => cmd_update_apply(),
        },
        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
        Cmd::Fork {
            src,
            new_id,
            subnet,
            delete_source,
        } => cmd_fork(&src, &new_id, &subnet, delete_source),
        Cmd::Resources { action } => cmd_resources(action),
        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
        Cmd::Shutdown { id } => cmd_shutdown(id),
        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),
        Cmd::Api(args) => crate::api::run(args),
        Cmd::Daemon => cmd_daemon(),
    }
}

        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
        Cmd::Whoami => cmd_whoami(),
        Cmd::Subnet { action } => {
            match action.unwrap_or(SubnetCmd::Status {
                name: None,
                nodes: false,
            }) {
                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),
                SubnetCmd::Notify { body, target, from } => cmd_notify(body, target, from),
            }
        }
        Cmd::HowTo { topic } => cmd_how_to(topic.as_deref()),
        // The endpoint noun (M8 decisions 1–2): bare = the merged list.
        // [impl->REQ-CLI-1]
        Cmd::Endpoint { action } => match action.unwrap_or(EndpointCmd::List {
            local: false,
            subnet: None,
            detail: false,
        }) {
            EndpointCmd::List {
                local,
                subnet,
                detail,
            } => cmd_endpoint_list(local, subnet, detail),
            EndpointCmd::Fork {
                src,
                new_id,
                subnet,
                delete_source,
            } => cmd_fork(&src, &new_id, &subnet, delete_source),
            EndpointCmd::Suspend { id } => {
                cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED")
            }
            EndpointCmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
            EndpointCmd::Shutdown { id } => cmd_shutdown(id),
            EndpointCmd::Stop { id } => cmd_stop(&id),
            EndpointCmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),
            EndpointCmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
            EndpointCmd::Access { action } => cmd_access(action),
            EndpointCmd::Description { action } => {
                cmd_description(action.unwrap_or(DescriptionCmd::Show { id: None }))
            }
        },
        Cmd::Grant { action } => cmd_grant(action),
        Cmd::Adapter { action } => cmd_adapter(action),
        Cmd::Shell { action } => cmd_shell(action),
        Cmd::Notif { action } => match action {
            NotifCmd::List { subnet } => cmd_notif_list(subnet),
            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),
        },
        Cmd::Update { action } => match action {
            UpdateCmd::Apply => cmd_update_apply(),
        },
        Cmd::Api(args) => crate::api::run(args),
        // The daemon noun (M8 decisions 5, 25b): bare = the node status view.
        // [impl->REQ-CLI-2]
        Cmd::Daemon { action } => match action.unwrap_or(DaemonCmd::Status) {
            DaemonCmd::Run => cmd_daemon_run(),
            DaemonCmd::Stop => cmd_daemon_stop(),
            DaemonCmd::Status => cmd_daemon_status(),
        },
    }
}

-        Cmd::Stop { id } => cmd_stop(&id),

+                SubnetCmd::Notify { body, target, from } => cmd_notify(body, target, from),

-        Cmd::Access { action } => cmd_access(action),

+        // The endpoint noun (M8 decisions 1–2): bare = the merged list.

+        Cmd::Endpoint { action } => match action.unwrap_or(EndpointCmd::List {

+            } => cmd_endpoint_list(local, subnet, detail),

+            } => cmd_fork(&src, &new_id, &subnet, delete_source),

+            EndpointCmd::Suspend { id } => {

+                cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED")

+            EndpointCmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),

+            EndpointCmd::Shutdown { id } => cmd_shutdown(id),

+            EndpointCmd::Stop { id } => cmd_stop(&id),

+            EndpointCmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),

+            EndpointCmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),

+            EndpointCmd::Access { action } => cmd_access(action),

+            EndpointCmd::Description { action } => {

+                cmd_description(action.unwrap_or(DescriptionCmd::Show { id: None }))

-        Cmd::Notify { body, subnet, from } => cmd_notify(body, subnet, from),

-        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),

-        } => cmd_fork(&src, &new_id, &subnet, delete_source),

-        Cmd::Resources { action } => cmd_resources(action),

-        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),

-        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),

-        Cmd::Shutdown { id } => cmd_shutdown(id),

-        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),

+        // The daemon noun (M8 decisions 5, 25b): bare = the node status view.

+        Cmd::Daemon { action } => match action.unwrap_or(DaemonCmd::Status) {

+            DaemonCmd::Run => cmd_daemon_run(),

+            DaemonCmd::Stop => cmd_daemon_stop(),

+            DaemonCmd::Status => cmd_daemon_status(),