Edit

      - name: Sync repo-face files to spt-releases main
        # releases-repo/ is the truth for spt-releases' README + licenses
        # (ADR-0014: truth lives here). Push each only when it differs.
        env:
          RELEASES_TOKEN: ${{ secrets.RELEASES_TOKEN }}
        run: |
          for f in README.md LICENSE-MIT LICENSE-BINARY; do
            local_b64=$(base64 -w0 "releases-repo/$f")
            remote_json=$(GH_TOKEN="$RELEASES_TOKEN" gh api \
              "repos/SaberMage/spt-releases/contents/$f" 2>/dev/null || echo '{}')
            remote_b64=$(echo "$remote_json" | jq -r '.content // ""' | tr -d '\n')
            remote_sha=$(echo "$remote_json" | jq -r '.sha // ""')
            if [ "$local_b64" != "$remote_b64" ]; then
              echo "syncing $f -> spt-releases main"
              GH_TOKEN="$RELEASES_TOKEN" gh api -X PUT \
                "repos/SaberMage/spt-releases/contents/$f" \
                -f message="docs: sync $f from spt-core@${GITHUB_SHA::12}" \
                -f content="$local_b64" \
                ${remote_sha:+-f sha="$remote_sha"} >/dev/null
            else
              echo "$f up to date"
            fi
          done

      - name: Push to spt-releases gh-pages


The file C:\Users\decid\Documents\projects\spt-core\.github\workflows\docs-publish.yml has been updated successfully. (file state is current in your context — no need to Read it back)

      - name: Sync repo-face files to spt-releases main
        # releases-repo/ is the truth for spt-releases' README + licenses
        # (ADR-0014: truth lives here). Push each only when it differs.
        env:
          RELEASES_TOKEN: ${{ secrets.RELEASES_TOKEN }}
        run: |
          for f in README.md LICENSE-MIT LICENSE-BINARY; do
            local_b64=$(base64 -w0 "releases-repo/$f")
            remote_json=$(GH_TOKEN="$RELEASES_TOKEN" gh api \
              "repos/SaberMage/spt-releases/contents/$f" 2>/dev/null || echo '{}')
            remote_b64=$(echo "$remote_json" | jq -r '.content // ""' | tr -d '\n')
            remote_sha=$(echo "$remote_json" | jq -r '.sha // ""')
            if [ "$local_b64" != "$remote_b64" ]; then
              echo "syncing $f -> spt-releases main"
              GH_TOKEN="$RELEASES_TOKEN" gh api -X PUT \
                "repos/SaberMage/spt-releases/contents/$f" \
                -f message="docs: sync $f from spt-core@${GITHUB_SHA::12}" \
                -f content="$local_b64" \
                ${remote_sha:+-f sha="$remote_sha"} >/dev/null
            else
              echo "$f up to date"
            fi
          done

      - name: Push to spt-releases gh-pages

# M6-D5: the source-repo publish pipeline (ADR-0014 topology — doc truth +
# generation live HERE; spt-releases' gh-pages branch is a pure rendered
# snapshot, force-pushed whole each publish; history lives in this repo).
#
# Triggers: release tags (the v0.1.0 ship publishes the matching docs) and
# manual dispatch (docs-only refreshes between releases).
#
# Needs the RELEASES_TOKEN repo secret: a fine-grained PAT with
# Contents:read/write on SaberMage/spt-releases only.
#
# [impl->REQ-REL-1]
# [impl->REQ-DOCS-1]
name: docs-publish

on:
  push:
    tags: ["v*"]
  workflow_dispatch:

jobs:
  publish:
    runs-on: [self-hosted, Linux, gravity]
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5
      - name: Ensure mdbook
        # Self-healing like the traceability install: gravity keeps a cargo
        # toolchain; install once, reuse thereafter.
        run: |
          if ! command -v mdbook >/dev/null && [ ! -x "$HOME/.cargo/bin/mdbook" ]; then
            cargo install mdbook --locked
          fi
          export PATH="$HOME/.cargo/bin:$PATH"
          mdbook --version
      - name: Drift gates (CLI ref + llms links + schema)
        run: |
          cargo run -p xtask -- check
          cargo test -q -p spt-runtime checked_in_schema
      - name: Assemble publish tree
        run: |
          export PATH="$HOME/.cargo/bin:$PATH"
          cargo run -p xtask -- site "$RUNNER_TEMP/site"
      - name: Push to spt-releases gh-pages
        env:
          RELEASES_TOKEN: ${{ secrets.RELEASES_TOKEN }}
        run: |
          cd "$RUNNER_TEMP/site"
          git init -q -b gh-pages
          git -c user.name="spt-core-ci" -c user.email="ci@users.noreply.github.com" \
            add -A
          git -c user.name="spt-core-ci" -c user.email="ci@users.noreply.github.com" \
            commit -q -m "docs: publish from spt-core@${GITHUB_SHA::12} (${GITHUB_REF_NAME})"
          git push --force \
            "https://x-access-token:${RELEASES_TOKEN}@github.com/SaberMage/spt-releases" \
            gh-pages

+      - name: Sync repo-face files to spt-releases main

+        # releases-repo/ is the truth for spt-releases' README + licenses

+        # (ADR-0014: truth lives here). Push each only when it differs.

+          RELEASES_TOKEN: ${{ secrets.RELEASES_TOKEN }}

+          for f in README.md LICENSE-MIT LICENSE-BINARY; do

+            local_b64=$(base64 -w0 "releases-repo/$f")

+            remote_json=$(GH_TOKEN="$RELEASES_TOKEN" gh api \

+              "repos/SaberMage/spt-releases/contents/$f" 2>/dev/null || echo '{}')

+            remote_b64=$(echo "$remote_json" | jq -r '.content // ""' | tr -d '\n')

+            remote_sha=$(echo "$remote_json" | jq -r '.sha // ""')

+            if [ "$local_b64" != "$remote_b64" ]; then

+              echo "syncing $f -> spt-releases main"

+              GH_TOKEN="$RELEASES_TOKEN" gh api -X PUT \

+                "repos/SaberMage/spt-releases/contents/$f" \

+                -f message="docs: sync $f from spt-core@${GITHUB_SHA::12}" \

+                -f content="$local_b64" \

+                ${remote_sha:+-f sha="$remote_sha"} >/dev/null


/// Filled at the v0.1.0 key ceremony (`cargo run -p xtask -- release-keygen`,
/// run by the maintainer; the seeds never touch this repo). Until then the
/// builtin set is empty and verification stays fail-closed exactly as before.
///
/// `release-keys.json` (identity dir) *overlays* this set: file keys add to
/// it, file revocations void builtin ids too — so a compromised builtin key
/// is still revocable per node without a rebuild.
// [impl->REQ-REL-3]
pub const BUILTIN_RELEASE_KEYS: &[(&str, &str)] = &[
    // ("rel-primary-2026", "<64-hex verifying key — minted at the ceremony>"),
    // ("rel-recovery-2026", "<64-hex verifying key — minted at the ceremony>"),
];

/// Minted at the v0.1.0 key ceremony (2026-06-05, `xtask release-keygen`,
/// run by the maintainer; both seeds custodied off-machine — the seeds never
/// touch this repo or CI).
///
/// `release-keys.json` (identity dir) *overlays* this set: file keys add to
/// it, file revocations void builtin ids too — so a compromised builtin key
/// is still revocable per node without a rebuild.
// [impl->REQ-REL-3]
pub const BUILTIN_RELEASE_KEYS: &[(&str, &str)] = &[
    (
        "rel-primary-2026",
        "54188f127f9c361090c90f85ac072e623d7001b311d25eca0dd5f57ad05e3ec9",
    ),
    (
        "rel-recovery-2026",
        "bd8908b9cbb0a1c308dd0bfb514596dd9de9ea64841528100d65efa96e834ab7",
    ),
];


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

/// Filled at the v0.1.0 key ceremony (`cargo run -p xtask -- release-keygen`,
/// run by the maintainer; the seeds never touch this repo). Until then the
/// builtin set is empty and verification stays fail-closed exactly as before.
///
/// `release-keys.json` (identity dir) *overlays* this set: file keys add to
/// it, file revocations void builtin ids too — so a compromised builtin key
/// is still revocable per node without a rebuild.
// [impl->REQ-REL-3]
pub const BUILTIN_RELEASE_KEYS: &[(&str, &str)] = &[
    // ("rel-primary-2026", "<64-hex verifying key — minted at the ceremony>"),
    // ("rel-recovery-2026", "<64-hex verifying key — minted at the ceremony>"),
];

/// Minted at the v0.1.0 key ceremony (2026-06-05, `xtask release-keygen`,
/// run by the maintainer; both seeds custodied off-machine — the seeds never
/// touch this repo or CI).
///
/// `release-keys.json` (identity dir) *overlays* this set: file keys add to
/// it, file revocations void builtin ids too — so a compromised builtin key
/// is still revocable per node without a rebuild.
// [impl->REQ-REL-3]
pub const BUILTIN_RELEASE_KEYS: &[(&str, &str)] = &[
    (
        "rel-primary-2026",
        "54188f127f9c361090c90f85ac072e623d7001b311d25eca0dd5f57ad05e3ec9",
    ),
    (
        "rel-recovery-2026",
        "bd8908b9cbb0a1c308dd0bfb514596dd9de9ea64841528100d65efa96e834ab7",
    ),
];

/// seed backups; rotates in a fresh primary if the primary is ever lost).

/// `(key_id, verifying-key-hex)` pairs.

-/// Filled at the v0.1.0 key ceremony (`cargo run -p xtask -- release-keygen`,

-/// run by the maintainer; the seeds never touch this repo). Until then the

-/// builtin set is empty and verification stays fail-closed exactly as before.

+/// Minted at the v0.1.0 key ceremony (2026-06-05, `xtask release-keygen`,

+/// run by the maintainer; both seeds custodied off-machine — the seeds never

-    // ("rel-primary-2026", "<64-hex verifying key — minted at the ceremony>"),

-    // ("rel-recovery-2026", "<64-hex verifying key — minted at the ceremony>"),

/// On-disk trusted-release-keys config (`identity/release-keys.json`) — the


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; cargo test -q -p spt-daemon shipped_builtin 2>&1 | Select-String "test result"

Validate embedded keys parse


test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 174 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s


i made some adjustments to the spt-releases README.md. please fetch it and sync it back to this repo (if it has a copy here) so the changes aren't lost during future updates/releases. --- __rel-primar…


Keys embedded + validated. Help-text polish pass — locating the public-facing strings:


Get-ChildItem C:\Users\decid\Documents\projects\spt-core\crates\spt\src | Select-Object Name, Length

spt crate source layout


Name         Length
----         ------
api
cli.rs       165144
elevation.rs 6654
main.rs      639
roster.rs    2725
testutil.rs  1705
wansend.rs   29220


M6-D1: spt-releases repo + public face (README, licenses)

Create SaberMage/spt-releases private via gh; author README.md per grill decision 7; LICENSE-MIT + LICENSE-BINARY; Pages config deferred to D7 flip. Activate REQ-REL-1 doc stage.

Creating spt-releases repo + public face

M6-D2: installer scripts (install.sh / install.ps1)

Non-interactive: latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → print absolute path. Idempotent. CI rung against staged artifact + spt --version. Activate REQ-INSTALL-1/2/3/5.

M6-D3: manifest JSON Schema (schemars) + drift gate

schemars derives on spt-runtime manifest types; emit manifest.schema.json at stable site-root path + release asset; CI regenerate+diff gate. Activate REQ-DOCS-4 (partial), REQ-DOCS-5.

M6-D4: docs-site Tier 1 content (mdBook)

mdBook skeleton + Starlight-inspired theme; mental model; human quickstart; adapter quickstart (mock-adapter); harness-contract vertical complete; shell getting-started (spt-shell-notify); stubs for remaining verticals. Activate REQ-DOCS-1/2/3.

M6-D5: agent-export + generation plumbing

llms.txt + llms-full.txt; raw .md alongside HTML; CLI ref from clap; rustdoc → Pages; source-repo CI publish job → spt-releases Pages branch; drift gates. REQ-REL-1 impl, REQ-DOCS-4/5 complete.

M6-D6: release pipeline + signing ceremony

Workspace → 0.1.0; tag-triggered build matrix both runners; cross-repo release via fine-grained token; key ceremony per ADR-0015 (primary + recovery, embed both pubkeys in VerifyPolicy trusted set, seed backups); manual SignedRelease signing. Activate REQ-REL-2/3.

M6-D7: v0.1.0 ship + flip public + acceptance run

Tag v0.1.0; ceremony; publish release; flip spt-releases public; Pages live; acceptance proof per M6-PLAN (clean-machine install, quickstarts, llms.txt + schema fetch, staged v0.1.1 verify, recovery-key verify).


76:        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
88:        /// Home subnet for a NEW endpoint (REQ-INST-15; see `ready`).
92:    /// List perches under the owlery (id, state, address, ready, alive).
101:    /// Subnet pairing operations (REQ-PAIR-*). Local-only here — the cross-node
102:    /// ceremony rides spt-net (M4 D4+).
108:    /// at-a-glance "what is this agent doing now" view (ADR-0008). Pulls a
110:    /// `[subnet:]id@node` cross-node form is M4).
121:    /// Rename an endpoint's logical id, rippled on-disk (REQ-INST-11 / ADR-0006
132:    /// Endpoint access whitelist (ADR-0009, REQ-SEC-1): which origin nodes may
140:    /// Consent grant store (REQ-CONSENT-1): which agents hold which gated
142:    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask
148:    /// Adapter registration (REQ-INSTALL-4): the node-local registered set
157:    /// Shell instances (CONTEXT §Shell model, REQ-EP-5): mint, list, and tear
165:    /// Issue a subnet-wide user notification (ADR-0007, REQ-NOTIF-2): produced
180:    /// Inspect and acknowledge notifications (ADR-0007). Dismissal is the
193:    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,
194:    /// ADR-0010: home is immutable — fork is the cross-subnet move, never a
198:    /// in v1 — the remote arm composes with M5 instantiate-anywhere's
213:    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text
221:    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's
225:    /// `id@node` suspend-from-anywhere arm is M5.
230:    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the
238:    /// Gracefully shut down an agent's own endpoint (REQ-SHELL-2, CONTEXT
246:    /// Harness-contract inbound surface (REQ-API-1): the entry points a
249:    /// Run the per-machine broker/brain daemon (REQ-DAEMON-3). Internal: an
255:/// `spt access <action>` subcommands (REQ-SEC-1).
281:/// `spt adapter <action>` subcommands (REQ-INSTALL-4).
312:/// `spt grant <action>` subcommands (REQ-CONSENT-1).
344:/// `spt shell <action>` subcommands (M5-D3, REQ-EP-5). Owner-scoped: every
351:    /// perch under `owlery/<owner>/shells/`, starting offline (the broker
422:/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).
449:/// `spt notif <action>` subcommands (ADR-0007).
465:/// `spt update <action>` subcommands (REQ-UPD-4).
476:    /// node-bound fetch, gated behind OS elevation (REQ-PAIR-3/6). With no flag the
531:/// (REQ-TERM-4 / ADR-0008). Ensures the daemon is up, then pulls a snapshot or
533:/// only: a qualified `[subnet:]id@node` form is refused (cross-node is M4).
589:/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
593:///    same-name fork is the M5 remote arm);
595:/// 3. source exists locally (the remote-source arm is M5);
706:/// (D9-4, REQ-INST-14). `set`/`show` operate the LOCAL endpoint's authored
710:/// appears (ADR-0006 §6, exclusion by construction).
803:/// (REQ-INST-3): apply the event against the local endpoint's durable record
806:/// pull). A qualified `[subnet:]id@node` rides the remote rest arm (M5-D5b,
807:/// REQ-INST-6): resolve the peer from the registry snapshots, dial, and let
873:/// `spt shutdown [id]` — the agent's graceful self-suspend verb (M5-D4d,
874:/// CONTEXT §Shell model: "an agent gracefully shuts down *its own* endpoint
952:/// `spt notify` — agent-issued subnet notif (ADR-0007, REQ-NOTIF-2): produce
1259:/// Run the listener loop (REQ-MSG-3 lifecycle, driven from the CLI). Blocks
1476:/// OS-elevation gate (REQ-PAIR-3/6). Wires the real store, clock, and elevation probe
1509:/// `spt rename <old> <new>` — the on-disk rename ripple (REQ-INST-11). Validates
1515:/// (`identity/trust/access.json`, ADR-0009). The daemon's gate re-reads the
1610:/// (REQ-INSTALL-4) over `spt_runtime::registry`. `add` is manifest-first
1625:    /// every subprocess gets a timeout (REQ-HAZARD-SUBPROCESS-TIMEOUT).
1767:/// surface (M5-D3a, REQ-EP-5). Spawn **mints** identity (`<adapter>-<n>` +
1771:/// (M5-D4a). Owner-scoped throughout: refs only resolve within the owner's
2304:/// Run a shell-link action against the shell's node (M5-D8c): resolve the
2306:/// there), dial, and let the target run the action in its own owlery.
2379:/// The interactive affordance for an unapproved `shell spawn` ask (M5-D3d):
2450:/// (`identity/trust/grants.json`, REQ-CONSENT-1). Rows are scoped to **this


1	//! CLI surface + dispatch for the `spt` binary (REQ-MSG-2).
2	//!
3	//! Stable arg shapes and exit codes are the contract here. Each subcommand is a
4	//! thin shim: read I/O, call the matching `spt-msg` entry point, render the
5	//! structured outcome, return an exit code. No messaging logic lives in this
6	//! layer.
7	//!
8	//! Exit codes: `0` success (delivered, queued, replied, listed) · `1` runtime
9	//! refusal (no perch, empty body, unidentifiable self) · `2` usage error
10	//! (clap, for unknown/missing args).
11	// [impl->REQ-MSG-2]
12
13	use std::io::{Read, Write};
14	use std::path::PathBuf;
15	use std::time::Duration;
16
17	use clap::{Parser, Subcommand};
18	use spt_msg::deliver::{self, SendOutcome};
19	use spt_msg::ready::ReadyAgent;
20	use spt_msg::ring::{self, RingOutcome};
21	use spt_net::net::pairing::totp::TotpSeed;
22	use spt_store::perch::{self, ParentHint};
23	use spt_store::registry;
24	use spt_store::subnet::{SubnetError, SubnetStore};
25
26	use crate::elevation::{self, Elevation, Gate};
27	use crate::roster;
28
29	/// Product tag embedded in the `otpauth://` provisioning URI (the authenticator-app
30	/// issuer label). Stable user-facing string.
31	const ISSUER: &str = "spt";
32
33	/// `spt pair show-totp` refused the fetch for lack of OS elevation. Distinct from the
34	/// crate-wide codes (`0` ok · `1` runtime refusal · `2` usage): `3` says the fetch was
35	/// *refused, not failed* — re-run elevated, or read the code from your authenticator app.
36	const EXIT_NOT_ELEVATED: i32 = 3;
37
38	#[derive(Parser)]
39	#[command(name = "spt", version, about = "spt-core local messaging", long_about = None)]
40	struct Cli {
41	    #[command(subcommand)]
42	    cmd: Cmd,
43	}
44
45	#[derive(Subcommand)]
46	enum Cmd {
47	    /// Send a message (body read from stdin). Fire-and-forget.
48	    Send {
49	        /// Target perch id. May be omitted when --reply-to is given.
50	        target: Option<String>,
51	        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
52	        #[arg(long)]
53	        from: Option<String>,
54	        /// Reply to this sender; sets the target and labels output REPLIED.
55	        #[arg(long)]
56	        reply_to: Option<String>,
57	        /// Spool only (deferred / hook channel) — no live wake.
58	        #[arg(long)]
59	        deferred: bool,
60	    },
61	    /// Send and block for a reply (body read from stdin; reply printed to stdout).
62	    Ring {
63	        /// Target perch id.
64	        target: String,
65	        /// Sender id (auto-detected from session if omitted).
66	        #[arg(long)]
67	        from: Option<String>,
68	        /// Seconds to wait for a reply before giving up.
69	        #[arg(long, default_value = "60")]
70	        timeout: u64,
71	    },
72	    /// Become reachable: register the perch and listen for messages (blocks).
73	    Ready {
74	        /// This agent's perch id.
75	        id: String,
76	        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
77	        /// multi-subnet node — home is assigned at creation, no guessing).
78	        #[arg(long)]
79	        subnet: Option<String>,
80	    },
81	    /// Receive messages: drain backlog then listen, printing each to stdout.
82	    Poll {
83	        /// This agent's perch id.
84	        id: String,
85	        /// Run a single drain+receive cycle, then exit (legacy one-shot).
86	        #[arg(long)]
87	        once: bool,
88	        /// Home subnet for a NEW endpoint (REQ-INST-15; see `ready`).
89	        #[arg(long)]
90	        subnet: Option<String>,
91	    },
92	    /// List perches under the owlery (id, state, address, ready, alive).
93	    List,
94	    /// Soft-stop a perch: remove the ready marker + unregister (spool preserved).
95	    Stop {
96	        /// Perch id to stop.
97	        id: String,
98	    },
99	    /// Print this session's own perch id (from $OWL_SESSION_ID / $SPT_AGENT_ID).
100	    Whoami,
101	    /// Subnet pairing operations (REQ-PAIR-*). Local-only here — the cross-node
102	    /// ceremony rides spt-net (M4 D4+).
103	    Pair {
104	        #[command(subcommand)]
105	        action: PairCmd,
106	    },
107	    /// Show a daemon-hosted session's live activity buffer (PTY digest) — the
108	    /// at-a-glance "what is this agent doing now" view (ADR-0008). Pulls a
109	    /// snapshot, or `--follow`s the delta-stream. Local addressing only (the
110	    /// `[subnet:]id@node` cross-node form is M4).
111	    Digest {
112	        /// The (local) endpoint id to read.
113	        id: String,
114	        /// Stream live changes instead of a one-shot snapshot (Ctrl-C to stop).
115	        #[arg(long)]
116	        follow: bool,
117	        /// Emit structured JSON instead of the human-glanceable render.
118	        #[arg(long)]
119	        json: bool,
120	    },
121	    /// Rename an endpoint's logical id, rippled on-disk (REQ-INST-11 / ADR-0006
122	    /// §2): the Self perch dir, its nested psyche/worker perches, and every
123	    /// `info.json`. Refuses while the perch is live (stop it first). The subnet
124	    /// registry rows ripple when the daemon-held registry lands (D4); the
125	    /// `a-<id>` context branch with the context store (D6).
126	    Rename {
127	        /// The endpoint's current (bare) id.
128	        old_id: String,
129	        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
130	        new_id: String,
131	    },
132	    /// Endpoint access whitelist (ADR-0009, REQ-SEC-1): which origin nodes may
133	    /// send an endpoint unsolicited off-node inbound. Absent entry = open;
134	    /// `allow` flips the endpoint to restricted; revoking the last node leaves
135	    /// it locked down; `open` deletes the restriction.
136	    Access {
137	        #[command(subcommand)]
138	        action: AccessCmd,
139	    },
140	    /// Consent grant store (REQ-CONSENT-1): which agents hold which gated
141	    /// capabilities on this node. Default-deny (the access whitelist's
142	    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask
143	    /// escalates interactively; `add` is the durable allow-always answer.
144	    Grant {
145	        #[command(subcommand)]
146	        action: GrantCmd,
147	    },
148	    /// Adapter registration (REQ-INSTALL-4): the node-local registered set
149	    /// under `{SPT_HOME}/adapters/` — what this node can drive/launch (one
150	    /// command for harness and shell adapters). The registered set feeds
151	    /// creation-time adapter selection, shell discovery, and the self-update
152	    /// ripple.
153	    Adapter {
154	        #[command(subcommand)]
155	        action: AdapterCmd,
156	    },
157	    /// Shell instances (CONTEXT §Shell model, REQ-EP-5): mint, list, and tear
158	    /// down the driven surfaces this agent owns. `spawn` MINTS a new instance
159	    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing an
160	    /// existing offline instance back is `relink` / `persistent` / wake.
161	    Shell {
162	        #[command(subcommand)]
163	        action: ShellCmd,
164	    },
165	    /// Issue a subnet-wide user notification (ADR-0007, REQ-NOTIF-2): produced
166	    /// into the replicated notif spool and first-fired at the user's
167	    /// most-recently-active endpoint in that subnet. Body from the trailing
168	    /// arg, or stdin when omitted.
169	    Notify {
170	        /// Notification body (read from stdin when omitted).
171	        body: Option<String>,
172	        /// Which subnet to notify. Required only when the node holds several
173	        /// (no guessing — the `pair show-totp` stance).
174	        #[arg(long)]
175	        subnet: Option<String>,
176	        /// Issuer endpoint id (auto-detected from session if omitted).
177	        #[arg(long)]
178	        from: Option<String>,
179	    },
180	    /// Inspect and acknowledge notifications (ADR-0007). Dismissal is the
181	    /// explicit ack — it latches and replicates subnet-wide.
182	    Notif {
183	        #[command(subcommand)]
184	        action: NotifCmd,
185	    },
186	    /// Self-update operations. `apply` is the explicit ack named by the
187	    /// update-consent notification; it re-verifies the staged release before
188	    /// touching the live daemon.
189	    Update {
190	        #[command(subcommand)]
191	        action: UpdateCmd,
192	    },
193	    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,
194	    /// ADR-0010: home is immutable — fork is the cross-subnet move, never a
195	    /// re-home). Seeds the fork with a one-time copy of the source's mind
196	    /// (live + project tiers); the two diverge immediately (no ongoing
197	    /// sync). The source is untouched unless --delete-source. Same-node only
198	    /// in v1 — the remote arm composes with M5 instantiate-anywhere's
199	    /// consent gate, and the same-name default rides it (one node holds one
200	    /// perch per name, so a local fork needs a new id).
201	    Fork {
202	        /// The source endpoint (must exist on this node).
203	        src: String,
204	        /// The fork's id (must differ from the source on the same node).
205	        new_id: String,
206	        /// The fork's home subnet — the target (must be a member).
207	        #[arg(long)]
208	        subnet: String,
209	        /// Delete the source endpoint (perch + tracked mind) after the copy.
210	        #[arg(long)]
211	        delete_source: bool,
212	    },
213	    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text
214	    /// service blurbs — an agent yellow-pages. `set` authors your own,
215	    /// `show` reads a local endpoint's, `list` projects the registry view
216	    /// over visible rows.
217	    Resources {
218	        #[command(subcommand)]
219	        action: ResourcesCmd,
220	    },
221	    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's
222	    /// suspend edge. From dormant — or straight from active, in which case
223	    /// the transition echo commune still fires (gate-armed for the
224	    /// endpoint's own pulse loop). Local addressing only — the qualified
225	    /// `id@node` suspend-from-anywhere arm is M5.
226	    Suspend {
227	        /// The (local) endpoint id to suspend.
228	        id: String,
229	    },
230	    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the
231	    /// existing seat (state's already there — no fresh spawn), resurfaces
232	    /// undismissed notifs at the Wake boundary, and requests an immediate
233	    /// context freshness pull from trusted peers. Local addressing only.
234	    Wake {
235	        /// The (local) endpoint id to wake.
236	        id: String,
237	    },
238	    /// Gracefully shut down an agent's own endpoint (REQ-SHELL-2, CONTEXT
239	    /// §Shell model): soft-stop the listener, then the suspend edge — the
240	    /// transition echo-commune fires (gate-armed) and persistent shells
241	    /// cascade offline. `spt suspend` without the session-over half.
242	    Shutdown {
243	        /// The endpoint id (defaults to the session's own perch).
244	        id: Option<String>,
245	    },
246	    /// Harness-contract inbound surface (REQ-API-1): the entry points a
247	    /// harness's hooks fire to mutate on-disk SPT state.
248	    Api(crate::api::ApiArgs),
249	    /// Run the per-machine broker/brain daemon (REQ-DAEMON-3). Internal: an
250	    /// `api` invocation auto-starts this; operators rarely run it directly.
251	    #[command(hide = true)]
252	    Daemon,
253	}
254
255	/// `spt access <action>` subcommands (REQ-SEC-1).
256	#[derive(Subcommand)]
257	enum AccessCmd {
258	    /// Whitelist a node for an endpoint (creates the restriction if absent).
259	    Allow {
260	        /// The endpoint to restrict.
261	        endpoint: String,
262	        /// The origin node's pubkey hex (as shown by `spt list` / pairing).
263	        node: String,
264	    },
265	    /// Remove a node from an endpoint's whitelist. Never widens: revoking the
266	    /// last node leaves the endpoint locked down (all unsolicited refused).
267	    Revoke {
268	        endpoint: String,
269	        node: String,
270	    },
271	    /// Delete an endpoint's restriction entirely — back to default-open.
272	    Open {
273	        endpoint: String,
274	    },
275	    /// List restrictions (all endpoints, or one).
276	    List {
277	        endpoint: Option<String>,
278	    },
279	}
280
281	/// `spt adapter <action>` subcommands (REQ-INSTALL-4).
282	#[derive(Subcommand)]
283	enum AdapterCmd {
284	    /// Register an adapter from a local path (a dir holding `manifest.toml`,
285	    /// or the manifest file itself) or from GitHub (`--github user/repo`,
286	    /// cloned under `adapters/_github/`). Manifest-first: an invalid manifest
287	    /// registers nothing. Install is the first update — the declared
288	    /// `[update]` avenue is conducted once after recording.
289	    Add {
290	        /// Local manifest source (omit when using --github).
291	        path: Option<String>,
292	        /// GitHub source `user/repo` — manifest-first, then install via the
293	        /// declared `[update]` avenue.
294	        #[arg(long)]
295	        github: Option<String>,
296	    },
297	    /// Soft-deregister: hidden from new-creation/discovery; existing and live
298	    /// instances keep running. The manifest's optional `uninstall` template
299	    /// is conducted only with --force until adapter-linked quiesce detection
300	    /// lands (D3).
301	    Remove {
302	        name: String,
303	        /// Conduct the manifest `uninstall` template now, without waiting for
304	        /// quiesce.
305	        #[arg(long)]
306	        force: bool,
307	    },
308	    /// List registered adapters (active and soft-deregistered).
309	    List,
310	}
311
312	/// `spt grant <action>` subcommands (REQ-CONSENT-1).
313	#[derive(Subcommand)]
314	enum GrantCmd {
315	    /// Record a grant: `agent` may exercise `capability` on this node.
316	    /// Refuses the reserved deferred capability ids (remote-exec,
317	    /// instantiate-anywhere) — their gate refuses unconditionally, so a row
318	    /// would only be a footgun-in-waiting.
319	    Add {
320	        /// The gated capability id (e.g. spawn-shell, owner-shutdown).
321	        capability: String,
322	        /// The subject agent (endpoint id).
323	        agent: String,
324	        /// Narrower target within the node (e.g. the shell-adapter name for
325	        /// spawn-shell). Omitted = the node-wide row; the two never match
326	        /// each other.
327	        #[arg(long)]
328	        qualifier: Option<String>,
329	    },
330	    /// Remove the exact grant row. Never widens or narrows neighbours: only
331	    /// the named (capability, agent, qualifier) tuple goes.
332	    Revoke {
333	        capability: String,
334	        agent: String,
335	        #[arg(long)]
336	        qualifier: Option<String>,
337	    },
338	    /// List grant rows (all, or one agent's).
339	    List {
340	        agent: Option<String>,
341	    },
342	}
343
344	/// `spt shell <action>` subcommands (M5-D3, REQ-EP-5). Owner-scoped: every
345	/// action operates on the invoking agent's own instances (`--owner`
346	/// overrides; auto-detected from the session otherwise).
347	#[derive(Subcommand)]
348	enum ShellCmd {
349	    /// Mint a NEW shell instance of a registered `kind="shell"` adapter:
350	    /// canonical id `<adapter>-<n>` (smallest free n; teardown frees slots),
351	    /// perch under `owlery/<owner>/shells/`, starting offline (the broker
352	    /// launch + bind handshake online it — D3b).
353	    Spawn {
354	        /// The providing shell adapter (must be registered + active).
355	        adapter: String,
356	        /// Optional owner-unique friendly label (interchangeable with the
357	        /// canonical id for addressing; never obscures the adapter).
358	        #[arg(long)]
359	        alias: Option<String>,
360	        /// Owning endpoint id (auto-detected from session if omitted).
361	        #[arg(long)]
362	        owner: Option<String>,
363	    },
364	    /// List this owner's instances: canonical id, alias, adapter, status.
365	    List {
366	        #[arg(long)]
367	        owner: Option<String>,
368	    },
369	    /// Destroy an instance (perch removed; mint slot + alias freed).
370	    Teardown {
371	        /// Canonical id or alias.
372	        shell_ref: String,
373	        #[arg(long)]
374	        owner: Option<String>,
375	    },
376	    /// Set/replace an instance's alias (owner-unique).
377	    Rename {
378	        /// Canonical id or current alias.
379	        shell_ref: String,
380	        /// The new alias.
381	        alias: String,
382	        #[arg(long)]
383	        owner: Option<String>,
384	    },
385	    /// Drive the shell with a typed capability command (the durable command
386	    /// channel): the op + positional args are vocabulary-checked against the
387	    /// manifest's `[shell.capabilities]`, spooled on the shell perch, and
388	    /// drained by the manifest's `command_receipt` mode (relay / stdin).
389	    Cmd {
390	        /// Canonical id or alias.
391	        shell_ref: String,
392	        /// The capability op + args (vocabulary-checked against the manifest).
393	        #[arg(trailing_var_arg = true)]
394	        op: Vec<String>,
395	        #[arg(long)]
396	        owner: Option<String>,
397	    },
398	    /// Send a text and/or file payload down the durable 2-way text+file
399	    /// channel (agent→shell; the shell answers via ordinary `spt send`).
400	    /// File transfers are progress-queryable by xfer id.
401	    Send {
402	        /// Canonical id or alias.
403	        shell_ref: String,
404	        /// The text payload.
405	        text: Option<String>,
406	        /// A file to transfer to the shell.
407	        #[arg(long)]
408	        file: Option<PathBuf>,
409	        #[arg(long)]
410	        owner: Option<String>,
411	    },
412	    /// Bring an existing offline (persistent) instance back online: re-spawns
413	    /// the binary with a fresh link token; the perch onlines at its bind.
414	    Relink {
415	        /// Canonical id or alias.
416	        shell_ref: String,
417	        #[arg(long)]
418	        owner: Option<String>,
419	    },
420	}
421
422	/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).
423	#[derive(Subcommand)]
424	enum ResourcesCmd {
425	    /// Author this endpoint's blurb (the agent refines its own at runtime;
426	    /// an empty string clears it back to the node-config seed).
427	    Set {
428	        /// The blurb text ("" clears).
429	        text: String,
430	        /// Which local endpoint (auto-detected from the session if omitted).
431	        #[arg(long)]
432	        id: Option<String>,
433	    },
434	    /// Show a local endpoint's authored blurb.
435	    Show {
436	        /// The local endpoint id (auto-detected if omitted).
437	        id: Option<String>,
438	    },
439	    /// The subnet resource-registry projection: `(id, node, status, blurb)`
440	    /// over VISIBLE rows only (hidden endpoints never appear — discovery
441	    /// leaks nothing a viewer couldn't reach).
442	    List {
443	        /// Limit to one subnet (all member subnets otherwise).
444	        #[arg(long)]
445	        subnet: Option<String>,
446	    },
447	}
448
449	/// `spt notif <action>` subcommands (ADR-0007).
450	#[derive(Subcommand)]
451	enum NotifCmd {
452	    /// List notifications (all member subnets, or one).
453	    List {
454	        /// Limit to one subnet.
455	        #[arg(long)]
456	        subnet: Option<String>,
457	    },
458	    /// Dismiss (ack) a notification by id — latches, replicates subnet-wide.
459	    Dismiss {
460	        /// The notif id (as shown by `spt notif list`).
461	        notif_id: String,
462	    },
463	}
464
465	/// `spt update <action>` subcommands (REQ-UPD-4).
466	#[derive(Subcommand)]
467	enum UpdateCmd {
468	    /// Apply the staged, verified self-update now.
469	    Apply,
470	}
471
472	/// `spt pair <action>` subcommands.
473	#[derive(Subcommand)]
474	enum PairCmd {
475	    /// Show a subnet's current 6-digit pairing code + `otpauth://` URI — the
476	    /// node-bound fetch, gated behind OS elevation (REQ-PAIR-3/6). With no flag the
477	    /// node's sole subnet is used; if it holds several, `--subnet <name>` is required
478	    /// (no guessing). `--create-new <name>` mints a fresh subnet first.
479	    ShowTotp {
480	        /// Which subnet's code to show. Required only when the node holds several.
481	        #[arg(long)]
482	        subnet: Option<String>,
483	        /// Mint a fresh subnet with this name (this node becomes the sole seed-holder),
484	        /// then show its code. Mutually exclusive with `--subnet`.
485	        #[arg(long, value_name = "NAME", conflicts_with = "subnet")]
486	        create_new: Option<String>,
487	    },
488	}
489
490	/// Parse args and dispatch. Returns the process exit code.
491	pub fn run() -> i32 {
492	    let cli = Cli::parse();
493	    match cli.cmd {
494	        Cmd::Send { target, from, reply_to, deferred } => cmd_send(target, from, reply_to, deferred),
495	        Cmd::Ring { target, from, timeout } => cmd_ring(target, from, timeout),
496	        Cmd::Ready { id, subnet } => run_listen(&id, false, subnet.as_deref()),
497	        Cmd::Poll { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
498	        Cmd::List => cmd_list(),
499	        Cmd::Stop { id } => cmd_stop(&id),
500	        Cmd::Whoami => cmd_whoami(),
501	        Cmd::Pair { action } => match action {
502	            PairCmd::ShowTotp { subnet, create_new } => cmd_pair_show_totp(subnet, create_new),
503	        },
504	        Cmd::Access { action } => cmd_access(action),
505	        Cmd::Grant { action } => cmd_grant(action),
506	        Cmd::Adapter { action } => cmd_adapter(action),
507	        Cmd::Shell { action } => cmd_shell(action),
508	        Cmd::Notify { body, subnet, from } => cmd_notify(body, subnet, from),
509	        Cmd::Notif { action } => match action {
510	            NotifCmd::List { subnet } => cmd_notif_list(subnet),
511	            NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),
512	        },
513	        Cmd::Update { action } => match action {
514	            UpdateCmd::Apply => cmd_update_apply(),
515	        },
516	        Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),
517	        Cmd::Fork { src, new_id, subnet, delete_source } => {
518	            cmd_fork(&src, &new_id, &subnet, delete_source)
519	        }
520	        Cmd::Resources { action } => cmd_resources(action),
521	        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
522	        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),
523	        Cmd::Shutdown { id } => cmd_shutdown(id),
524	        Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),
525	        Cmd::Api(args) => crate::api::run(args),
526	        Cmd::Daemon => cmd_daemon(),
527	    }
528	}
529
530	/// `spt digest <id>` — show a daemon-hosted session's live activity buffer
531	/// (REQ-TERM-4 / ADR-0008). Ensures the daemon is up, then pulls a snapshot or
532	/// follows the delta-stream over the digest-control channel. Local addressing
533	/// only: a qualified `[subnet:]id@node` form is refused (cross-node is M4).
534	// [impl->REQ-TERM-4]
535	fn cmd_digest(id: &str, follow: bool, json: bool) -> i32 {
536	    // Local addressing only — the qualified cross-node form is M4 (spt-net).
537	    if id.contains('@') || id.contains(':') {
538	        eprintln!("CROSS_NODE_M4:{id} — qualified [subnet:]id@node addressing is M4, not yet supported");
539	        return 1;
540	    }

//! CLI surface + dispatch for the `spt` binary (REQ-MSG-2).
//!
//! Stable arg shapes and exit codes are the contract here. Each subcommand is a
//! thin shim: read I/O, call the matching `spt-msg` entry point, render the
//! structured outcome, return an exit code. No messaging logic lives in this
//! layer.
//!
//! Exit codes: `0` success (delivered, queued, replied, listed) · `1` runtime
//! refusal (no perch, empty body, unidentifiable self) · `2` usage error
//! (clap, for unknown/missing args).
// [impl->REQ-MSG-2]

use std::io::{Read, Write};
use std::path::PathBuf;
use std::time::Duration;

use clap::{Parser, Subcommand};
use spt_msg::deliver::{self, SendOutcome};
use spt_msg::ready::ReadyAgent;
use spt_msg::ring::{self, RingOutcome};
use spt_net::net::pairing::totp::TotpSeed;
use spt_store::perch::{self, ParentHint};
use spt_store::registry;
use spt_store::subnet::{SubnetError, SubnetStore};

use crate::elevation::{self, Elevation, Gate};
use crate::roster;

/// Product tag embedded in the `otpauth://` provisioning URI (the authenticator-app
/// issuer label). Stable user-facing string.
const ISSUER: &str = "spt";

/// `spt pair show-totp` refused the fetch for lack of OS elevation. Distinct from the
/// crate-wide codes (`0` ok · `1` runtime refusal · `2` usage): `3` says the fetch was
/// *refused, not failed* — re-run elevated, or read the code from your authenticator app.
const EXIT_NOT_ELEVATED: i32 = 3;

#[derive(Parser)]
#[command(name = "spt", version, about = "spt-core local messaging", long_about = None)]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// Send a message (body read from stdin). Fire-and-forget.
    Send {
        /// Target perch id. May be omitted when --reply-to is given.
        target: Option<String>,
        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
        /// Reply to this sender; sets the target and labels output REPLIED.
        #[arg(long)]
        reply_to: Option<String>,
        /// Spool only (deferred / hook channel) — no live wake.
        #[arg(long)]
        deferred: bool,
    },
    /// Send and block for a reply (body read from stdin; reply printed to stdout).
    Ring {
        /// Target perch id.
        target: String,
        /// Sender id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
        /// Seconds to wait for a reply before giving up.
        #[arg(long, default_value = "60")]
        timeout: u64,
    },
    /// Become reachable: register the perch and listen for messages (blocks).
    Ready {
        /// This agent's perch id.
        id: String,
        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
        /// multi-subnet node — home is assigned at creation, no guessing).
        #[arg(long)]
        subnet: Option<String>,
    },
    /// Receive messages: drain backlog then listen, printing each to stdout.
    Poll {
        /// This agent's perch id.
        id: String,
        /// Run a single drain+receive cycle, then exit (legacy one-shot).
        #[arg(long)]
        once: bool,
        /// Home subnet for a NEW endpoint (REQ-INST-15; see `ready`).
        #[arg(long)]
        subnet: Option<String>,
    },
    /// List perches under the owlery (id, state, address, ready, alive).
    List,
    /// Soft-stop a perch: remove the ready marker + unregister (spool preserved).
    Stop {
        /// Perch id to stop.
        id: String,
    },
    /// Print this session's own perch id (from $OWL_SESSION_ID / $SPT_AGENT_ID).
    Whoami,
    /// Subnet pairing operations (REQ-PAIR-*). Local-only here — the cross-node
    /// ceremony rides spt-net (M4 D4+).
    Pair {
        #[command(subcommand)]
        action: PairCmd,
    },
    /// Show a daemon-hosted session's live activity buffer (PTY digest) — the
    /// at-a-glance "what is this agent doing now" view (ADR-0008). Pulls a
    /// snapshot, or `--follow`s the delta-stream. Local addressing only (the
    /// `[subnet:]id@node` cross-node form is M4).
    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, rippled on-disk (REQ-INST-11 / ADR-0006
    /// §2): the Self perch dir, its nested psyche/worker perches, and every
    /// `info.json`. Refuses while the perch is live (stop it first). The subnet
    /// registry rows ripple when the daemon-held registry lands (D4); the
    /// `a-<id>` context branch with the context store (D6).
    Rename {
        /// The endpoint's current (bare) id.
        old_id: String,
        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
        new_id: String,
    },
    /// Endpoint access whitelist (ADR-0009, REQ-SEC-1): 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 (REQ-CONSENT-1): which agents hold which gated
    /// capabilities on this node. Default-deny (the access whitelist's
    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask
    /// escalates interactively; `add` is the durable allow-always answer.
    Grant {
        #[command(subcommand)]
        action: GrantCmd,
    },
    /// Adapter registration (REQ-INSTALL-4): the node-local registered set
    /// under `{SPT_HOME}/adapters/` — what this node can drive/launch (one
    /// command for harness and shell adapters). The registered set feeds
    /// creation-time adapter selection, shell discovery, and the self-update
    /// ripple.
    Adapter {
        #[command(subcommand)]
        action: AdapterCmd,
    },
    /// Shell instances (CONTEXT §Shell model, REQ-EP-5): mint, list, and tear
    /// down the driven surfaces this agent owns. `spawn` MINTS a new instance
    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing an
    /// existing offline instance back is `relink` / `persistent` / wake.
    Shell {
        #[command(subcommand)]
        action: ShellCmd,
    },
    /// Issue a subnet-wide user notification (ADR-0007, REQ-NOTIF-2): produced
    /// into the replicated notif 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
        /// (no guessing — the `pair show-totp` stance).
        #[arg(long)]
        subnet: Option<String>,
        /// Issuer endpoint id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
    },
    /// Inspect and acknowledge notifications (ADR-0007). Dismissal is the
    /// explicit ack — it latches and replicates subnet-wide.
    Notif {
        #[command(subcommand)]
        action: NotifCmd,
    },
    /// Self-update operations. `apply` is the explicit ack named by the
    /// update-consent notification; it re-verifies the staged release before
    /// touching the live daemon.
    Update {
        #[command(subcommand)]
        action: UpdateCmd,
    },
    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,
    /// ADR-0010: home is 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
    /// in v1 — the remote arm composes with M5 instantiate-anywhere's
    /// consent gate, and the same-name default rides it (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 (REQ-INST-14): per-endpoint free-text
    /// service blurbs — an agent yellow-pages. `set` authors your own,
    /// `show` reads a local endpoint's, `list` projects the registry view
    /// over visible rows.
    Resources {
        #[command(subcommand)]
        action: ResourcesCmd,
    },
    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's
    /// suspend edge. From dormant — or straight from active, in which case
    /// the transition echo commune still fires (gate-armed for the
    /// endpoint's own pulse loop). Local addressing only — the qualified
    /// `id@node` suspend-from-anywhere arm is M5.
    Suspend {
        /// The (local) endpoint id to suspend.
        id: String,
    },
    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the
    /// existing seat (state's already there — no fresh spawn), resurfaces
    /// undismissed notifs at the Wake boundary, and requests an immediate
    /// context freshness pull from trusted peers. Local addressing only.
    Wake {
        /// The (local) endpoint id to wake.
        id: String,
    },
    /// Gracefully shut down an agent's own endpoint (REQ-SHELL-2, CONTEXT
    /// §Shell model): soft-stop the listener, then the suspend edge — the
    /// transition echo-commune fires (gate-armed) and persistent shells
    /// cascade offline. `spt suspend` without the session-over half.
    Shutdown {
        /// The endpoint id (defaults to the session's own perch).
        id: Option<String>,
    },
    /// Harness-contract inbound surface (REQ-API-1): the entry points a
    /// harness's hooks fire to mutate on-disk SPT state.
    Api(crate::api::ApiArgs),
    /// Run the per-machine broker/brain daemon (REQ-DAEMON-3). Internal: an
    /// `api` invocation auto-starts this; operators rarely run it directly.
    #[command(hide = true)]
    Daemon,
}

/// `spt access <action>` subcommands (REQ-SEC-1).
#[derive(Subcommand)]
enum AccessCmd {
    /// Whitelist a node for an endpoint (creates the restriction if absent).
    Allow {
        /// The endpoint to restrict.
        endpoint: String,
        /// The origin node's pubkey hex (as shown by `spt list` / pairing).
        node: String,
    },
    /// Remove a node from an endpoint's whitelist. Never widens: revoking the
    /// last node leaves the endpoint locked down (all unsolicited refused).
    Revoke {
        endpoint: String,
        node: String,
    },
    /// Delete an endpoint's restriction entirely — back to default-open.
    Open {
        endpoint: String,
    },
    /// List restrictions (all endpoints, or one).
    List {
        endpoint: Option<String>,
    },
}

/// `spt adapter <action>` subcommands (REQ-INSTALL-4).
#[derive(Subcommand)]
enum AdapterCmd {
    /// Register an adapter from a local path (a dir holding `manifest.toml`,
    /// or the manifest file itself) or from GitHub (`--github user/repo`,
    /// cloned under `adapters/_github/`). Manifest-first: an invalid manifest
    /// registers nothing. Install is the first update — the declared
    /// `[update]` avenue is conducted once after recording.
    Add {
        /// Local manifest source (omit when using --github).
        path: Option<String>,
        /// GitHub source `user/repo` — manifest-first, then install via the
        /// declared `[update]` avenue.
        #[arg(long)]
        github: Option<String>,
    },
    /// Soft-deregister: hidden from new-creation/discovery; existing and live
    /// instances keep running. The manifest's optional `uninstall` template
    /// is conducted only with --force until adapter-linked quiesce detection
    /// lands (D3).
    Remove {
        name: String,
        /// Conduct the manifest `uninstall` template now, without waiting for
        /// quiesce.
        #[arg(long)]
        force: bool,
    },
    /// List registered adapters (active and soft-deregistered).
    List,
}

/// `spt grant <action>` subcommands (REQ-CONSENT-1).
#[derive(Subcommand)]
enum GrantCmd {
    /// Record a grant: `agent` may exercise `capability` on this node.
    /// Refuses the reserved deferred capability ids (remote-exec,
    /// instantiate-anywhere) — their gate refuses unconditionally, so a row
    /// would only be a footgun-in-waiting.
    Add {
        /// The gated capability id (e.g. spawn-shell, owner-shutdown).
        capability: String,
        /// The subject agent (endpoint id).
        agent: String,
        /// Narrower target within the node (e.g. the shell-adapter name for
        /// spawn-shell). Omitted = the node-wide row; the two never match
        /// each other.
        #[arg(long)]
        qualifier: Option<String>,
    },
    /// Remove the exact grant row. Never widens or narrows neighbours: only
    /// the named (capability, agent, qualifier) tuple goes.
    Revoke {
        capability: String,
        agent: String,
        #[arg(long)]
        qualifier: Option<String>,
    },
    /// List grant rows (all, or one agent's).
    List {
        agent: Option<String>,
    },
}

/// `spt shell <action>` subcommands (M5-D3, REQ-EP-5). Owner-scoped: every
/// action operates on the invoking agent's own instances (`--owner`
/// overrides; auto-detected from the session otherwise).
#[derive(Subcommand)]
enum ShellCmd {
    /// Mint a NEW shell instance of a registered `kind="shell"` adapter:
    /// canonical id `<adapter>-<n>` (smallest free n; teardown frees slots),
    /// perch under `owlery/<owner>/shells/`, starting offline (the broker
    /// launch + bind handshake online it — D3b).
    Spawn {
        /// The providing shell adapter (must be registered + active).
        adapter: String,
        /// Optional owner-unique friendly label (interchangeable with the
        /// canonical id for addressing; never obscures the adapter).
        #[arg(long)]
        alias: Option<String>,
        /// Owning endpoint id (auto-detected from session if omitted).
        #[arg(long)]
        owner: Option<String>,
    },
    /// List this owner's instances: canonical id, alias, adapter, status.
    List {
        #[arg(long)]
        owner: Option<String>,
    },
    /// Destroy an instance (perch removed; mint slot + alias freed).
    Teardown {
        /// Canonical id or alias.
        shell_ref: String,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Set/replace an instance's alias (owner-unique).
    Rename {
        /// Canonical id or current alias.
        shell_ref: String,
        /// The new alias.
        alias: String,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Drive the shell with a typed capability command (the durable command
    /// channel): the op + positional args are vocabulary-checked against the
    /// manifest's `[shell.capabilities]`, spooled on the shell perch, and
    /// drained by the manifest's `command_receipt` mode (relay / stdin).
    Cmd {
        /// Canonical id or alias.
        shell_ref: String,
        /// The capability op + args (vocabulary-checked against the manifest).
        #[arg(trailing_var_arg = true)]
        op: Vec<String>,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Send a text and/or file payload down the durable 2-way text+file
    /// channel (agent→shell; the shell answers via ordinary `spt send`).
    /// File transfers are progress-queryable by xfer id.
    Send {
        /// Canonical id or alias.
        shell_ref: String,
        /// The text payload.
        text: Option<String>,
        /// A file to transfer to the shell.
        #[arg(long)]
        file: Option<PathBuf>,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Bring an existing offline (persistent) instance back online: re-spawns
    /// the binary with a fresh link token; the perch onlines at its bind.
    Relink {
        /// Canonical id or alias.
        shell_ref: String,
        #[arg(long)]
        owner: Option<String>,
    },
}

/// `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).
#[derive(Subcommand)]
enum NotifCmd {
    /// List notifications (all member subnets, or one).
    List {
        /// Limit to one subnet.
        #[arg(long)]
        subnet: Option<String>,
    },
    /// Dismiss (ack) a notification by id — latches, replicates subnet-wide.
    Dismiss {
        /// The notif id (as shown by `spt notif list`).
        notif_id: String,
    },
}

/// `spt update <action>` subcommands (REQ-UPD-4).
#[derive(Subcommand)]
enum UpdateCmd {
    /// Apply the staged, verified self-update now.
    Apply,
}

/// `spt pair <action>` subcommands.
#[derive(Subcommand)]
enum PairCmd {
    /// Show a subnet's current 6-digit pairing code + `otpauth://` URI — the
    /// node-bound fetch, gated behind OS elevation (REQ-PAIR-3/6). With no flag the
    /// node's sole subnet is used; if it holds several, `--subnet <name>` is required
    /// (no guessing). `--create-new <name>` mints a fresh subnet first.
    ShowTotp {
        /// Which subnet's code to show. Required only when the node holds several.
        #[arg(long)]
        subnet: Option<String>,
        /// Mint a fresh subnet with this name (this node becomes the sole seed-holder),
        /// then show its code. Mutually exclusive with `--subnet`.
        #[arg(long, value_name = "NAME", conflicts_with = "subnet")]
        create_new: Option<String>,
    },
}

/// Parse args and dispatch. Returns the process exit code.
pub fn run() -> i32 {
    let cli = Cli::parse();
    match cli.cmd {
        Cmd::Send { target, from, reply_to, deferred } => cmd_send(target, from, reply_to, deferred),
        Cmd::Ring { target, from, timeout } => cmd_ring(target, from, timeout),
        Cmd::Ready { id, subnet } => run_listen(&id, false, subnet.as_deref()),
        Cmd::Poll { 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::Pair { action } => match action {
            PairCmd::ShowTotp { subnet, create_new } => cmd_pair_show_totp(subnet, create_new),
        },
        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(),
    }
}

/// `spt digest <id>` — show a daemon-hosted session's live activity buffer
/// (REQ-TERM-4 / ADR-0008). Ensures the daemon is up, then pulls a snapshot or
/// follows the delta-stream over the digest-control channel. Local addressing
/// only: a qualified `[subnet:]id@node` form is refused (cross-node is M4).
// [impl->REQ-TERM-4]
fn cmd_digest(id: &str, follow: bool, json: bool) -> i32 {
    // Local addressing only — the qualified cross-node form is M4 (spt-net).
    if id.contains('@') || id.contains(':') {
        eprintln!("CROSS_NODE_M4:{id} — qualified [subnet:]id@node addressing is M4, not yet supported");
        return 1;
    }


crates\spt\src\cli.rs:812:fn cmd_rest(id: &str, event: spt_daemon::RestEvent, verb: &str) -> i32 {
crates\spt\src\cli.rs-813-    if id.contains('@') || id.contains(':') {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-814-        let tag = match event {
crates\spt\src\cli.rs-815-            spt_daemon::RestEvent::Suspend => spt_net::net::rest::REST_EVENT_SUSPEND,
crates\spt\src\cli.rs-816-            spt_daemon::RestEvent::Wake => spt_net::net::rest::REST_EVENT_WAKE,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-817-            // Only the manual verbs route here; the other events are
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-818-            // daemon-internal feeds with no CLI surface.
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-819-            _ => {
crates\spt\src\cli.rs-820-                eprintln!("{verb}_FAIL:{id}: event has no remote arm");
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-821-                return 1;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-822-            }
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-823-        };
crates\spt\src\cli.rs:824:        return match crate::wansend::wan_rest(id, tag) {
crates\spt\src\cli.rs-825-            crate::wansend::WanRestOutcome::Edge { node, detail } => {
crates\spt\src\cli.rs-826-                eprintln!("{verb}:{id} ({detail}) at {node}");
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-827-                0
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-828-            }
crates\spt\src\cli.rs-829-            crate::wansend::WanRestOutcome::NoEdge { node } => {
crates\spt\src\cli.rs-830-                eprintln!("NO_EDGE:{id} (already in the target state) at {node}");
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-831-                0
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-832-            }
crates\spt\src\cli.rs-833-            crate::wansend::WanRestOutcome::NoReply { node } => {
crates\spt\src\cli.rs-834-                eprintln!("{verb}_REFUSED:{id}: {node} closed the stream without a reply (access gate)");
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-835-                1
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-836-            }
crates\spt\src\cli.rs-837-            crate::wansend::WanRestOutcome::RemoteFail { node, detail } => {
crates\spt\src\cli.rs-838-                eprintln!("{verb}_FAIL:{id} at {node}: {detail}");
--
crates\spt\src\wansend.rs:162:pub fn wan_rest(target: &str, event_tag: &str) -> WanRestOutcome {
crates\spt\src\wansend.rs-163-    let own_hex = match spt_store::nodeid::load_or_create() {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-164-        Ok(id) => id.public_key().to_hex(),
crates\spt\src\wansend.rs-165-        Err(e) => return WanRestOutcome::Failed(format!("node identity: {e}")),
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-166-    };
crates\spt\src\wansend.rs-167-    let regs = load_snapshots(&perch::identity_dir().join("registry"));
crates\spt\src\wansend.rs-168-    if spt_daemon::daemon::ensure_running().is_err() {
crates\spt\src\wansend.rs-169-        return WanRestOutcome::Failed("daemon did not come up".to_string());
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-170-    }
crates\spt\src\wansend.rs-171-    let mut brain = match Brain::cold_start(&broker_socket_name(), now_ms()) {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-172-        Ok(b) => b,
crates\spt\src\wansend.rs-173-        Err(e) => return WanRestOutcome::Failed(format!("broker connect: {e}")),

[Showing results with pagination = limit: 40]


Confirmed — suspend/wake remote arm shipped; the help is stale. Applying the polish:


Edit

        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
        /// multi-subnet node — home is assigned at creation, no guessing).

        /// Home subnet for a NEW endpoint (required on a multi-subnet
        /// node — home is assigned at creation, never guessed).


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

        /// Home subnet for a NEW endpoint (REQ-INST-15: required on a
        /// multi-subnet node — home is assigned at creation, no guessing).

        /// Home subnet for a NEW endpoint (required on a multi-subnet
        /// node — home is assigned at creation, never guessed).

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

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

+        /// Home subnet for a NEW endpoint (required on a multi-subnet

+        /// node — home is assigned at creation, never guessed).


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

/// Home subnet for a NEW endpoint (see `ready`).


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)

/// Run a single drain+receive cycle, then exit (legacy one-shot).

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

+        /// Home subnet for a NEW endpoint (see `ready`).


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

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


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

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

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

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

/// Soft-stop a perch: remove the ready marker + unregister (spool preserved).


    /// Subnet pairing operations (REQ-PAIR-*). Local-only here — the cross-node
    /// ceremony rides spt-net (M4 D4+).
    Pair {

    /// Subnet pairing: show this node's current pairing code (and mint new
    /// subnets). Pairing is how machines join a private subnet.
    Pair {


    /// Subnet pairing operations (REQ-PAIR-*). Local-only here — the cross-node
    /// ceremony rides spt-net (M4 D4+).
    Pair {

    /// Subnet pairing: show this node's current pairing code (and mint new
    /// subnets). Pairing is how machines join a private subnet.
    Pair {

/// Print this session's own perch id (from $OWL_SESSION_ID / $SPT_AGENT_ID).

-    /// Subnet pairing operations (REQ-PAIR-*). Local-only here — the cross-node

-    /// ceremony rides spt-net (M4 D4+).

+    /// Subnet pairing: show this node's current pairing code (and mint new

+    /// subnets). Pairing is how machines join a private subnet.


    /// Show a daemon-hosted session's live activity buffer (PTY digest) — the
    /// at-a-glance "what is this agent doing now" view (ADR-0008). Pulls a
    /// snapshot, or `--follow`s the delta-stream. Local addressing only (the
    /// `[subnet:]id@node` cross-node form is M4).
    Digest {

    /// 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 {


    /// Show a daemon-hosted session's live activity buffer (PTY digest) — the
    /// at-a-glance "what is this agent doing now" view (ADR-0008). Pulls a
    /// snapshot, or `--follow`s the delta-stream. Local addressing only (the
    /// `[subnet:]id@node` cross-node form is M4).
    Digest {

    /// 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 {

-    /// Show a daemon-hosted session's live activity buffer (PTY digest) — the

-    /// at-a-glance "what is this agent doing now" view (ADR-0008). Pulls a

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

-    /// `[subnet:]id@node` cross-node form is M4).

+    /// 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.


    /// Rename an endpoint's logical id, rippled on-disk (REQ-INST-11 / ADR-0006
    /// §2): the Self perch dir, its nested psyche/worker perches, and every
    /// `info.json`. Refuses while the perch is live (stop it first). The subnet
    /// registry rows ripple when the daemon-held registry lands (D4); the
    /// `a-<id>` context branch with the context store (D6).
    Rename {

    /// Rename an endpoint's logical id, rippled across its on-disk state:
    /// 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 {


    /// Rename an endpoint's logical id, rippled on-disk (REQ-INST-11 / ADR-0006
    /// §2): the Self perch dir, its nested psyche/worker perches, and every
    /// `info.json`. Refuses while the perch is live (stop it first). The subnet
    /// registry rows ripple when the daemon-held registry lands (D4); the
    /// `a-<id>` context branch with the context store (D6).
    Rename {

    /// Rename an endpoint's logical id, rippled across its on-disk state:
    /// 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 {

-    /// Rename an endpoint's logical id, rippled on-disk (REQ-INST-11 / ADR-0006

-    /// §2): the Self perch dir, its nested psyche/worker perches, and every

-    /// `info.json`. Refuses while the perch is live (stop it first). The subnet

-    /// registry rows ripple when the daemon-held registry lands (D4); the

-    /// `a-<id>` context branch with the context store (D6).

+    /// Rename an endpoint's logical id, rippled across its on-disk state:

+    /// the endpoint's perch dir, its nested companion/worker perches, and

+    /// every record naming it. Refuses while the perch is live (stop it


    /// Endpoint access whitelist (ADR-0009, REQ-SEC-1): which origin nodes may
    /// send an endpoint unsolicited off-node inbound. Absent entry = open;

    /// Endpoint access whitelist: which origin nodes may send an endpoint
    /// unsolicited off-node inbound. Absent entry = open;


    /// Endpoint access whitelist (ADR-0009, REQ-SEC-1): which origin nodes may
    /// send an endpoint unsolicited off-node inbound. Absent entry = open;

    /// Endpoint access whitelist: which origin nodes may send an endpoint
    /// unsolicited off-node inbound. Absent entry = open;

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

-    /// Endpoint access whitelist (ADR-0009, REQ-SEC-1): which origin nodes may

-    /// send an endpoint unsolicited off-node inbound. Absent entry = open;

+    /// Endpoint access whitelist: 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.


    /// Consent grant store (REQ-CONSENT-1): which agents hold which gated
    /// capabilities on this node. Default-deny (the access whitelist's
    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask
    /// escalates interactively; `add` is the durable allow-always answer.
    Grant {

    /// Consent grant store: which agents hold which gated capabilities on
    /// this node. Default-deny (the access whitelist's opposite polarity).
    /// An ungranted ask escalates interactively; `add` is the durable
    /// allow-always answer.
    Grant {


    /// Consent grant store (REQ-CONSENT-1): which agents hold which gated
    /// capabilities on this node. Default-deny (the access whitelist's
    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask
    /// escalates interactively; `add` is the durable allow-always answer.
    Grant {

    /// Consent grant store: which agents hold which gated capabilities on
    /// this node. Default-deny (the access whitelist's opposite polarity).
    /// An ungranted ask escalates interactively; `add` is the durable
    /// allow-always answer.
    Grant {

-    /// Consent grant store (REQ-CONSENT-1): which agents hold which gated

-    /// capabilities on this node. Default-deny (the access whitelist's

-    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask

-    /// escalates interactively; `add` is the durable allow-always answer.

+    /// Consent grant store: which agents hold which gated capabilities on

+    /// this node. Default-deny (the access whitelist's opposite polarity).

+    /// An ungranted ask escalates interactively; `add` is the durable


    /// Adapter registration (REQ-INSTALL-4): the node-local registered set
    /// under `{SPT_HOME}/adapters/` — what this node can drive/launch (one
    /// command for harness and shell adapters). The registered set feeds
    /// creation-time adapter selection, shell discovery, and the self-update
    /// ripple.
    Adapter {

    /// Adapter registration: the node-local registered set — what this node
    /// can drive/launch (one command for harness and shell adapters). The
    /// registered set feeds creation-time adapter selection, shell
    /// discovery, and the self-update ripple.
    Adapter {


    /// Adapter registration (REQ-INSTALL-4): the node-local registered set
    /// under `{SPT_HOME}/adapters/` — what this node can drive/launch (one
    /// command for harness and shell adapters). The registered set feeds
    /// creation-time adapter selection, shell discovery, and the self-update
    /// ripple.
    Adapter {

    /// Adapter registration: the node-local registered set — what this node
    /// can drive/launch (one command for harness and shell adapters). The
    /// registered set feeds creation-time adapter selection, shell
    /// discovery, and the self-update ripple.
    Adapter {

-    /// Adapter registration (REQ-INSTALL-4): the node-local registered set

-    /// under `{SPT_HOME}/adapters/` — what this node can drive/launch (one

-    /// command for harness and shell adapters). The registered set feeds

-    /// creation-time adapter selection, shell discovery, and the self-update

+    /// Adapter registration: the node-local registered set — what this node

+    /// can drive/launch (one command for harness and shell adapters). The

+    /// registered set feeds creation-time adapter selection, shell

+    /// discovery, and the self-update ripple.


    /// Shell instances (CONTEXT §Shell model, REQ-EP-5): mint, list, and tear
    /// down the driven surfaces this agent owns. `spawn` MINTS a new instance
    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing an
    /// existing offline instance back is `relink` / `persistent` / wake.
    Shell {

    /// Shell instances: mint, list, drive, and tear down the driven surfaces
    /// this agent owns. `spawn` MINTS a new instance identity
    /// (`<adapter>-<n>`) — it is not the online switch; bringing an existing
    /// offline instance back is `relink` / `persistent` / wake.
    Shell {


    /// Shell instances (CONTEXT §Shell model, REQ-EP-5): mint, list, and tear
    /// down the driven surfaces this agent owns. `spawn` MINTS a new instance
    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing an
    /// existing offline instance back is `relink` / `persistent` / wake.
    Shell {

    /// Shell instances: mint, list, drive, and tear down the driven surfaces
    /// this agent owns. `spawn` MINTS a new instance identity
    /// (`<adapter>-<n>`) — it is not the online switch; bringing an existing
    /// offline instance back is `relink` / `persistent` / wake.
    Shell {

-    /// Shell instances (CONTEXT §Shell model, REQ-EP-5): mint, list, and tear

-    /// down the driven surfaces this agent owns. `spawn` MINTS a new instance

-    /// identity (`<adapter>-<n>`) — it is not the online switch; bringing an

-    /// existing offline instance back is `relink` / `persistent` / wake.

+    /// Shell instances: mint, list, drive, and tear down the driven surfaces

+    /// this agent owns. `spawn` MINTS a new instance identity

+    /// (`<adapter>-<n>`) — it is not the online switch; bringing an existing

+    /// offline instance back is `relink` / `persistent` / wake.


    /// Issue a subnet-wide user notification (ADR-0007, REQ-NOTIF-2): produced
    /// into the replicated notif 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 {

    /// 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 {


    /// Issue a subnet-wide user notification (ADR-0007, REQ-NOTIF-2): produced
    /// into the replicated notif 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 {

    /// 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 {

-    /// Issue a subnet-wide user notification (ADR-0007, REQ-NOTIF-2): produced

-    /// into the replicated notif spool and first-fired at the user's

-    /// most-recently-active endpoint in that subnet. Body from the trailing

+    /// 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

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


        /// Which subnet to notify. Required only when the node holds several
        /// (no guessing — the `pair show-totp` stance).

        /// Which subnet to notify. Required only when the node holds several
        /// (never guessed).


        /// Which subnet to notify. Required only when the node holds several
        /// (no guessing — the `pair show-totp` stance).

        /// Which subnet to notify. Required only when the node holds several
        /// (never guessed).

-        /// (no guessing — the `pair show-totp` stance).

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


    /// Inspect and acknowledge notifications (ADR-0007). Dismissal is the
    /// explicit ack — it latches and replicates subnet-wide.
    Notif {

    /// Inspect and acknowledge notifications. Dismissal is the explicit
    /// ack — it latches and replicates subnet-wide.
    Notif {


    /// Inspect and acknowledge notifications (ADR-0007). Dismissal is the
    /// explicit ack — it latches and replicates subnet-wide.
    Notif {

    /// Inspect and acknowledge notifications. Dismissal is the explicit
    /// ack — it latches and replicates subnet-wide.
    Notif {

-    /// Inspect and acknowledge notifications (ADR-0007). Dismissal is the

-    /// explicit ack — it latches and replicates subnet-wide.

+    /// Inspect and acknowledge notifications. Dismissal is the explicit

+    /// ack — it latches and replicates subnet-wide.


    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,
    /// ADR-0010: home is 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
    /// in v1 — the remote arm composes with M5 instantiate-anywhere's
    /// consent gate, and the same-name default rides it (one node holds one
    /// perch per name, so a local fork needs a new id).
    Fork {

    /// 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 {


    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,
    /// ADR-0010: home is 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
    /// in v1 — the remote arm composes with M5 instantiate-anywhere's
    /// consent gate, and the same-name default rides it (one node holds one
    /// perch per name, so a local fork needs a new id).
    Fork {

    /// 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 {

-    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,

-    /// ADR-0010: home is 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

-    /// in v1 — the remote arm composes with M5 instantiate-anywhere's

-    /// consent gate, and the same-name default rides it (one node holds one

-    /// perch per name, so a local fork needs a new id).

+    /// 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 +

+    /// 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).

/// The source endpoint (must exist on this node).


    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text
    /// service blurbs — an agent yellow-pages. `set` authors your own,
    /// `show` reads a local endpoint's, `list` projects the registry view
    /// over visible rows.
    Resources {

    /// The subnet resource registry: per-endpoint free-text service blurbs
    /// — an agent yellow-pages. `set` authors your own, `show` reads a
    /// local endpoint's, `list` projects the registry view over visible
    /// rows.
    Resources {


    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text
    /// service blurbs — an agent yellow-pages. `set` authors your own,
    /// `show` reads a local endpoint's, `list` projects the registry view
    /// over visible rows.
    Resources {

    /// The subnet resource registry: per-endpoint free-text service blurbs
    /// — an agent yellow-pages. `set` authors your own, `show` reads a
    /// local endpoint's, `list` projects the registry view over visible
    /// rows.
    Resources {

-    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text

-    /// service blurbs — an agent yellow-pages. `set` authors your own,

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

+    /// The subnet resource registry: per-endpoint free-text service blurbs

+    /// — an agent yellow-pages. `set` authors your own, `show` reads a

+    /// local endpoint's, `list` projects the registry view over visible


    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's
    /// suspend edge. From dormant — or straight from active, in which case
    /// the transition echo commune still fires (gate-armed for the
    /// endpoint's own pulse loop). Local addressing only — the qualified
    /// `id@node` suspend-from-anywhere arm is M5.
    Suspend {
        /// The (local) endpoint id to suspend.
        id: String,
    },

    /// Rest an endpoint cold: 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,
    },


    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's
    /// suspend edge. From dormant — or straight from active, in which case
    /// the transition echo commune still fires (gate-armed for the
    /// endpoint's own pulse loop). Local addressing only — the qualified
    /// `id@node` suspend-from-anywhere arm is M5.
    Suspend {
        /// The (local) endpoint id to suspend.
        id: String,
    },

    /// Rest an endpoint cold: 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,
    },

-    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's

-    /// suspend edge. From dormant — or straight from active, in which case

-    /// the transition echo commune still fires (gate-armed for the

-    /// endpoint's own pulse loop). Local addressing only — the qualified

-    /// `id@node` suspend-from-anywhere arm is M5.

+    /// Rest an endpoint cold: 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.

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

+        /// The endpoint id (qualified `id@node` reaches a paired peer).

/// Wake a resting local endpoint in place (REQ-INST-3): re-activates the


    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the
    /// existing seat (state's already there — no fresh spawn), resurfaces
    /// undismissed notifs at the Wake boundary, and requests an immediate
    /// context freshness pull from trusted peers. Local addressing only.
    Wake {
        /// The (local) endpoint id to wake.
        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,
    },


    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the
    /// existing seat (state's already there — no fresh spawn), resurfaces
    /// undismissed notifs at the Wake boundary, and requests an immediate
    /// context freshness pull from trusted peers. Local addressing only.
    Wake {
        /// The (local) endpoint id to wake.
        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,
    },

-    /// Wake a resting local endpoint in place (REQ-INST-3): re-activates the

-    /// existing seat (state's already there — no fresh spawn), resurfaces

-    /// undismissed notifs at the Wake boundary, and requests an immediate

-    /// context freshness pull from trusted peers. Local addressing only.

+    /// 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

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

/// Gracefully shut down an agent's own endpoint (REQ-SHELL-2, CONTEXT


    /// Gracefully shut down an agent's own endpoint (REQ-SHELL-2, CONTEXT
    /// §Shell model): soft-stop the listener, then the suspend edge — the
    /// transition echo-commune fires (gate-armed) and persistent shells
    /// cascade offline. `spt suspend` without the session-over half.
    Shutdown {

    /// Gracefully shut down an agent's own endpoint: soft-stop the listener,
    /// then the suspend edge — the final context save fires and persistent
    /// shells cascade offline with it.
    Shutdown {


    /// Gracefully shut down an agent's own endpoint (REQ-SHELL-2, CONTEXT
    /// §Shell model): soft-stop the listener, then the suspend edge — the
    /// transition echo-commune fires (gate-armed) and persistent shells
    /// cascade offline. `spt suspend` without the session-over half.
    Shutdown {

    /// Gracefully shut down an agent's own endpoint: soft-stop the listener,
    /// then the suspend edge — the final context save fires and persistent
    /// shells cascade offline with it.
    Shutdown {

-    /// Gracefully shut down an agent's own endpoint (REQ-SHELL-2, CONTEXT

-    /// §Shell model): soft-stop the listener, then the suspend edge — the

-    /// transition echo-commune fires (gate-armed) and persistent shells

-    /// cascade offline. `spt suspend` without the session-over half.

+    /// Gracefully shut down an agent's own endpoint: soft-stop the listener,

+    /// then the suspend edge — the final context save fires and persistent

+    /// shells cascade offline with it.

/// The endpoint id (defaults to the session's own perch).


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

    /// Harness-contract inbound surface: 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.


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

    /// Harness-contract inbound surface: 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.

-    /// Harness-contract inbound surface (REQ-API-1): the entry points a

-    /// harness's hooks fire to mutate on-disk SPT state.

+    /// Harness-contract inbound surface: the entry points a harness's hooks

+    /// fire to keep spt-core's on-disk state in sync.

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

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

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

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


    /// instances keep running. The manifest's optional `uninstall` template
    /// is conducted only with --force until adapter-linked quiesce detection
    /// lands (D3).

    /// instances keep running. The manifest's optional `uninstall` template
    /// is conducted only with --force until quiesce detection lands.


    /// instances keep running. The manifest's optional `uninstall` template
    /// is conducted only with --force until adapter-linked quiesce detection
    /// lands (D3).

    /// instances keep running. The manifest's optional `uninstall` template
    /// is conducted only with --force until quiesce detection lands.

/// Soft-deregister: hidden from new-creation/discovery; existing and live

-    /// is conducted only with --force until adapter-linked quiesce detection

+    /// is conducted only with --force until quiesce detection lands.

/// Conduct the manifest `uninstall` template now, without waiting for


    /// canonical id `<adapter>-<n>` (smallest free n; teardown frees slots),
    /// perch under `owlery/<owner>/shells/`, starting offline (the broker
    /// launch + bind handshake online it — D3b).

    /// canonical id `<adapter>-<n>` (smallest free n; teardown frees slots),
    /// starting offline (the launch + bind handshake brings it online).


    /// canonical id `<adapter>-<n>` (smallest free n; teardown frees slots),
    /// perch under `owlery/<owner>/shells/`, starting offline (the broker
    /// launch + bind handshake online it — D3b).

    /// canonical id `<adapter>-<n>` (smallest free n; teardown frees slots),
    /// starting offline (the launch + bind handshake brings it online).

/// Mint a NEW shell instance of a registered `kind="shell"` adapter:

-    /// perch under `owlery/<owner>/shells/`, starting offline (the broker

-    /// launch + bind handshake online it — D3b).

+    /// starting offline (the launch + bind handshake brings it online).

/// The providing shell adapter (must be registered + active).


    /// Show a subnet's current 6-digit pairing code + `otpauth://` URI — the
    /// node-bound fetch, gated behind OS elevation (REQ-PAIR-3/6). With no flag the
    /// node's sole subnet is used; if it holds several, `--subnet <name>` is required
    /// (no guessing). `--create-new <name>` mints a fresh subnet first.

    /// Show a subnet's current 6-digit pairing code + `otpauth://` URI —
    /// gated behind OS elevation (or read the code from your authenticator
    /// app). With no flag the node's sole subnet is used; if it holds
    /// several, `--subnet <name>` is required (never guessed).
    /// `--create-new <name>` mints a fresh subnet first.