here's the spec. includes command ideas that are meant to be deferred, but will be added in the future. [Image #8] assumed all are attached for now. --- net-less broker would show a status like "no connection" or "orphaned".


[Image: source: C:\Users\decid\Documents\ShareX\Screenshots\2026-06\Discord_HjDFDYepYp.png]


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


here's the spec. includes command ideas that are meant to be deferred, but will be added in the future. [Image #8] assumed all are attached for now. --- net-less broker would show a status like "no co…


Ratified: attached/detached = deliberate serve-state (deferred verbs), "no connection"/"orphaned" = degraded net-less state, all-attached assumed today. Writing glossary + deferred entries now — but first, one collision your spec just created:

**Q4: `spt shutdown` is already taken.**

Code today: `spt shutdown [id]` = an *agent gracefully shutting down its own endpoint* (Agent commands group, REQ-SHELL-2 — soft-stop listener, suspend edge, shell cascade). Your spec reuses it for *daemon* shutdown. Two different verbs can't share the name.

**Recommendation:** daemon lifecycle gets its own noun — `spt daemon stop` (and the existing hidden `spt daemon` becomes `spt daemon run`, hidden; room for `daemon status` later). Agent `shutdown` keeps its name — it shipped in v0.1.0's surface and matches CONTEXT's "agent shuts down *its own* endpoint" language. Deferred list records `spt daemon stop`, not `spt shutdown`.

Accept? (Then I write CONTEXT.md + DEFERRED.md, and build: `ensure` on create, the stderr coming-online banner with `spt subnets joined` section on create/join/digest/update-apply.)


agree with your recommendation. although by that logic, endpoint management commands should be nested under `spt identity`. i.e. `spt endpoint shutdown`, `spt endpoint fork`, `suspend`, `wake`, `ready`, `digest`, `resources`-->`spt endpoint list`, with `spt endpoint` alone serving as `whoami`. etc. etc. --- this logic also relocates `access` to i.e. `spt subnet node-whitelist` --- agree with these? challenge any? other ideas or proposals?


agree with your recommendation. although by that logic, endpoint management commands should be nested under `spt endpoint`. i.e. `spt endpoint shutdown`, `spt endpoint fork`, `suspend`, `wake`, `ready`, `digest`, `resources`-->`spt endpoint list`, with `spt endpoint` alone serving as `whoami`. etc. etc. --- this logic also relocates `access` to i.e. `spt subnet node-whitelist` --- agree with these? challenge any? other ideas or proposals?


Position per item — two agreements, two challenges, one sharpening, plus timing:

**Agree — nest management verbs under `spt endpoint`:** `fork`, `suspend`, `wake`, `shutdown`, `rename`, `stop`, `digest` are all endpoint-lifecycle ops; the noun namespace matches `subnet`'s precedent. (`spt endpoint shutdown` also dissolves the Q4 collision from the other side.)

**Challenge 1 — keep the agent hot path flat.** `send`, `ring`, `ready`, `whoami` are typed by agents constantly — they're in how-to texts, quickstart prompt blocks, harness hook templates. `spt endpoint ready doyle` taxes the highest-traffic call path for purity. Precedent: git nests plumbing, keeps porcelain hot verbs flat. The Agent/User help grouping (M7 D1) already separates the audiences — I'd make the *management* side structural and leave the hot path alone. (`ready` is arguably lifecycle, but ergonomics wins here.)

**Challenge 2 — `access` is endpoint-scoped, not subnet-scoped.** Code: `AccessStore` entries are keyed **per endpoint**, subjects are origin nodes ("which nodes may send unsolicited inbound *to endpoint X*"). `spt subnet node-whitelist` puts it under the wrong noun — there's no per-subnet whitelist anywhere. Correct home: `spt endpoint access allow|revoke|open|list`.

**Sharpening — `resources` → `spt endpoint list`:** two different things are fused today: `spt list` (local perches: id/state/ready/alive) and `resources list` (subnet-wide yellow-pages projection). Proposal: `spt endpoint list [--subnet <name>]` = one listing, local by default, subnet projection with the flag; the blurb authoring (`resources set/show`) becomes `spt endpoint blurb [set|show]`. And **bare `spt endpoint` = list**, not whoami — bare-noun-equals-status-view is the convention `spt subnet` just established; `whoami` stays flat (hot path).

**Other ideas while we're here:** `notify`/`notif` are subnet-scoped (they ride subnet replication) → candidates for `spt subnet notify` / `spt subnet notif`; `daemon` noun gets `stop|status` (+ hidden `run`).

**The ruling I need (timing):** this is a second surface-breaking reorg. Window's still open (no adapter yet), but M7 acceptance is mid-flight and quickstart/reference/e2e were *just* rewritten. Options: (a) fold into M7 before close — one more breaking commit, docs re-swept; (b) mint it as M8 "CLI nounification" with proper REQ + plan, executed immediately after M7 closes. **Recommend (b)** — same window, cleaner traceability, M7 acceptance stays a fixed target.

Rule on: hot-path exception, `endpoint access`, bare-endpoint=list, notify nesting, and (a)/(b)?


agree with your recommendation. although by that logic, endpoint management commands should be nested under `spt endpoint`. i.e. `spt endpoint shutdown`, `spt endpoint fork`, `suspend`, `wake`, `ready…


c1: agree. c2: agree. sharpening: mostly agree, but default all-subnets, endpoints grouped by subnet. `spt endpoint list [--local|--subnet <name>]`. also have it feature self endpoint at the top of the list, distinctly identified and separate from the other groups. mitigates the need for `whoami`. -change "blurb" to "description". no need for `show` verb, that'll be the behavior when `set` isn't used. -keep `notif` unburied, since it's about recalling the notifications that reached the user irrespective of subnet. -agree with `spt subnet notify`. --- ruling: agree with (b), but we need the daemon complimentary activation for M7 acceptance


All rulings locked. Executing the M7 slice now (banner + complimentary activation); M8 spec goes to the deferred registry. First the docs:


Bash

grep -n "^## \|^### " CONTEXT.md | sed -n 1,40p

Find CONTEXT.md glossary structure


11:## Language
41:### Deliverable shape
50:## Runtime model
81:### Manifest seams (outbound contract, detailed)
126:### Inbound `api` surface (detailed)
144:### Startup flows (the two topologies)
166:### Endpoint types
203:### Agent endpoints vs Shells
281:## Terminal wrapper
305:## Networking
320:### Cross-node Psyche sync
332:### Subnet notifications
350:## Self-update
362:## Instances
410:### Pieces the Instances model requires
442:### Off-node reach-back
449:### v1 scope line (V1-mid)
455:## Workspace & versioning
465:## Pairing & trust
519:## Consent & security gates
543:### Messaging substrate
548:## Installation
571:## Project infrastructure
580:### Frontend (day-one)


View end of Pairing & trust section


**subnet naming**:
The first time two nodes pair, the user is prompted to **name the subnet**. The name is a human label on the subnet identity (which is cryptographically the shared seed). On every subsequent pairing the same name is shown ("adding node to subnet `<name>`"), giving the user confidence they're extending the right subnet as the fleet grows.

**subnet icon** (GUI metadata): an optional small image (square PNG/WebP, ≤256 KB) representing the subnet in future GUIs. Stored **inline** in subnet metadata (so it syncs for free over the same subnet-material channel as the name; the cap keeps inline-sync trivial — it's an icon, not a media library), **editable by any node in the subnet, any time** (seeded at create/name). **GUI-only consumer** (frontend milestone); data distribution rides M4.

Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).

**local-link authentication**:
Cross-node traffic already rides Iroh (E2E-encrypted by node keypairs). The exposed surface is *same-node* channels where potentially-untrusted code touches SPT — a shell binary's HTTP/stdin/relay link, HTTP-to-harness-binary delivery. These require a **per-link handshake at `api bind` that establishes a link token + local-channel encryption**, required on every subsequent message — a local-link auth capability so other local processes can't inject into or read a link. Shell↔broker links specifically are encrypted + handshaked.

**binary-trust disclosure (accepted risk)**:
A shell binary is the least-trusted code in the system (3rd-party, runs on the user's node, agent-controllable) — but the same is true of harness binaries. The capability toolset bounds what an *agent* may ask; it does not sandbox what the *binary* may do with its OS permissions. Optional **shell-binary sandboxing** is deferred (gated like instantiate-anywhere/remote-exec). The baseline stance: **running any adapter or shell binary means trusting it — a disclosed, accepted risk for all spt-core users.**

## Consent & security gates


Edit

**subnet attachment (attached / detached)**:
Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state (future `spt subnet detach/attach [--auto]` verbs; `--auto` persists the startup default); a *degraded* daemon that cannot serve at all (net-less broker — endpoint bind failed) is rendered as **no connection**, never conflated with deliberate detachment. Until the detach verbs exist, every held subnet is attached whenever the daemon is up. Corollary, ratified 2026-06-06: **membership implies reachability** — the membership-creating verbs (`subnet create`, `subnet join`) ensure the daemon is running, because a sole seed-holder with no responder is a contradiction of the subnet's purpose.

**endpoint description** (canonical CLI term for the resource-advertisement blurb):
The per-endpoint free-text "yellow-pages" line (see §resource advertisement). Surface term is **description**; "blurb" survives only as the internal field name.

**local-link authentication**:


c1: agree. c2: agree. sharpening: mostly agree, but default all-subnets, endpoints grouped by subnet. `spt endpoint list [--local|--subnet <name>]`. also have it feature self endpoint at the top of th…


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

**subnet attachment (attached / detached)**:
Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state (future `spt subnet detach/attach [--auto]` verbs; `--auto` persists the startup default); a *degraded* daemon that cannot serve at all (net-less broker — endpoint bind failed) is rendered as **no connection**, never conflated with deliberate detachment. Until the detach verbs exist, every held subnet is attached whenever the daemon is up. Corollary, ratified 2026-06-06: **membership implies reachability** — the membership-creating verbs (`subnet create`, `subnet join`) ensure the daemon is running, because a sole seed-holder with no responder is a contradiction of the subnet's purpose.

**endpoint description** (canonical CLI term for the resource-advertisement blurb):
The per-endpoint free-text "yellow-pages" line (see §resource advertisement). Surface term is **description**; "blurb" survives only as the internal field name.

**local-link authentication**:

Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).

+**subnet attachment (attached / detached)**:

+Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state (future `spt subnet detach/attach [--auto]` verbs; `--auto` persists the startup default); a *degraded* daemon that cannot serve at all (net-less broker — endpoint bind failed) is rendered as **no connection**, never conflated with deliberate detachment. Until the detach verbs exist, every held subnet is attached whenever the daemon is up. Corollary, ratified 2026-06-06: **membership implies reachability** — the membership-creating verbs (`subnet create`, `subnet join`) ensure the daemon is running, because a sole seed-holder with no responder is a contradiction of the subnet's purpose.

+**endpoint description** (canonical CLI term for the resource-advertisement blurb):

+The per-endpoint free-text "yellow-pages" line (see §resource advertisement). Surface term is **description**; "blurb" survives only as the internal field name.

Cross-node traffic already rides Iroh (E2E-encrypted by node keypairs). The exposed surface is *same-node* channels where potentially-untrusted code touches SPT — a shell binary's HTTP/stdin/relay link, HTTP-to-harness-binary delivery. These require a **per-link handshake at `api bind` that establishes a link token + local-channel encryption**, required on every subsequent message — a local-link auth capability so other local processes can't inject into or read a link. Shell↔broker links specifically are encrypted + handshaked.


1	# Deferred Features
2
3	Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.
4
5	| Feature | Cut from | Why deferred | Trigger to revisit |
6	|---|---|---|---|
7	| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
8	| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
9	| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
10	| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
11	| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one MRA API (`spt-daemon::presence`). A future PresenceChannel's dispatch/bind/thread styles consume exactly this datum plus the broker PresenceLog seam (REQ-EP-4); nothing about the endpoint type needs a new gossip channel. | Once Shells exist and the subnet registry is stable |
12	| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
13	| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). <!-- [doc->REQ-INST-6] --> **M5-D5b note:** remote `spt suspend/wake <id@node>` shipped (the rest-op wire family, remote-drive trust class) — it *moves existing instances between rest states only*; the remote-**fork** arm (creating an instance on a node that has none) stays here, riding instantiate-anywhere's consent gate. A wake addressed to a node with no local instance refuses with `WAKE_NO_REACHABLE_INSTANCE` naming this deferral. | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
14	| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |
15	| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
16	| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |
17	| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |
18	| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |
19	| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
20	| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |
21

# Deferred Features

Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one MRA API (`spt-daemon::presence`). A future PresenceChannel's dispatch/bind/thread styles consume exactly this datum plus the broker PresenceLog seam (REQ-EP-4); nothing about the endpoint type needs a new gossip channel. | Once Shells exist and the subnet registry is stable |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). <!-- [doc->REQ-INST-6] --> **M5-D5b note:** remote `spt suspend/wake <id@node>` shipped (the rest-op wire family, remote-drive trust class) — it *moves existing instances between rest states only*; the remote-**fork** arm (creating an instance on a node that has none) stays here, riding instantiate-anywhere's consent gate. A wake addressed to a node with no local instance refuses with `WAKE_NO_REACHABLE_INSTANCE` naming this deferral. | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |
| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |
| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |
| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |


| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |

| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |
| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect, persisted flags) is its own deliverable | M8 CLI nounification, or first multi-subnet user needing selective participation |
| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |


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

| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |
| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect, persisted flags) is its own deliverable | M8 CLI nounification, or first multi-subnet user needing selective participation |
| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |

# Deferred Features

Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one MRA API (`spt-daemon::presence`). A future PresenceChannel's dispatch/bind/thread styles consume exactly this datum plus the broker PresenceLog seam (REQ-EP-4); nothing about the endpoint type needs a new gossip channel. | Once Shells exist and the subnet registry is stable |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). <!-- [doc->REQ-INST-6] --> **M5-D5b note:** remote `spt suspend/wake <id@node>` shipped (the rest-op wire family, remote-drive trust class) — it *moves existing instances between rest states only*; the remote-**fork** arm (creating an instance on a node that has none) stays here, riding instantiate-anywhere's consent gate. A wake addressed to a node with no local instance refuses with `WAKE_NO_REACHABLE_INSTANCE` naming this deferral. | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |
| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |
| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |
| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |

+| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect, persisted flags) is its own deliverable | M8 CLI nounification, or first multi-subnet user needing selective participation |

+| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |


Docs landed. Now the implementation — check exports first:


Bash

grep -n "pub use daemon::" crates/spt-daemon/src/lib.rs

Check daemon fn re-exports


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


Edit

/// `spt subnet [status [NAME] [--nodes]]` — the membership view (REQ-SUBNET-1,

/// Render the coming-online banner's "spt subnets joined" section + the
/// concluding divider: header, underline, one `NAME: <state>` row per held
/// subnet (`(none)` when empty), divider. Pure for tests.
// [impl->REQ-DAEMON-3]
fn render_subnets_joined(rows: &[(String, &'static str)]) -> String {
    let mut out = String::from("spt subnets joined\n──────────────────\n");
    if rows.is_empty() {
        out.push_str("  (none)\n");
    } else {
        for (name, state) in rows {
            out.push_str(&format!("  {name}: {state}\n"));
        }
    }
    out.push_str("────────────────────────────────────────\n");
    out
}

/// Ensure the per-machine daemon is up for a daemon-dependent command —
/// silent when it already runs; otherwise announce the transition on
/// stderr (stdout stays payload-pure) and conclude with a divider before
/// the command's own output (grill 2026-06-06). The membership-creating
/// verbs call this because **membership implies reachability** (CONTEXT
/// §subnet attachment): a sole seed-holder with no responder daemon is a
/// contradiction of the subnet's purpose.
// [impl->REQ-SUBNET-2] [impl->REQ-DAEMON-3]
fn ensure_daemon_announced() -> Result<(), i32> {
    if spt_daemon::is_running() {
        return Ok(());
    }
    eprintln!("spt-core daemon is offline - coming online...");
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_FAIL: {e}");
        return Err(1);
    }
    let ident = match (
        spt_store::hostlabel::os_hostname(),
        spt_store::nodeid::load_or_create().ok().map(|i| i.public_key().to_hex()),
    ) {
        (Some(l), Some(hex)) => {
            format!("{l} ({}…)", spt_net::net::registry::key_prefix(&hex))
        }
        (None, Some(hex)) => format!("{}…", spt_net::net::registry::key_prefix(&hex)),
        _ => "this node".to_string(),
    };
    eprintln!("spt-core daemon: online - {ident}");
    // Attachment states: every held subnet is attached while the daemon is
    // up (CONTEXT §subnet attachment — detach verbs are deferred); a
    // net-less daemon (endpoint bind failed) serves nothing: no connection.
    let net_up = spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms())
        .ok()
        .and_then(|mut b| b.net_status().ok())
        .map(|s| s.enabled)
        .unwrap_or(true); // can't ask ⇒ assume serving rather than alarm
    let state = if net_up { "attached" } else { "no connection" };
    let rows: Vec<(String, &'static str)> =
        SubnetStore::load().subnets.iter().map(|s| (s.name.clone(), state)).collect();
    eprint!("{}", render_subnets_joined(&rows));
    Ok(())
}

/// `spt subnet [status [NAME] [--nodes]]` — the membership view (REQ-SUBNET-1,


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)

/// Render the coming-online banner's "spt subnets joined" section + the
/// concluding divider: header, underline, one `NAME: <state>` row per held
/// subnet (`(none)` when empty), divider. Pure for tests.
// [impl->REQ-DAEMON-3]
fn render_subnets_joined(rows: &[(String, &'static str)]) -> String {
    let mut out = String::from("spt subnets joined\n──────────────────\n");
    if rows.is_empty() {
        out.push_str("  (none)\n");
    } else {
        for (name, state) in rows {
            out.push_str(&format!("  {name}: {state}\n"));
        }
    }
    out.push_str("────────────────────────────────────────\n");
    out
}

/// Ensure the per-machine daemon is up for a daemon-dependent command —
/// silent when it already runs; otherwise announce the transition on
/// stderr (stdout stays payload-pure) and conclude with a divider before
/// the command's own output (grill 2026-06-06). The membership-creating
/// verbs call this because **membership implies reachability** (CONTEXT
/// §subnet attachment): a sole seed-holder with no responder daemon is a
/// contradiction of the subnet's purpose.
// [impl->REQ-SUBNET-2] [impl->REQ-DAEMON-3]
fn ensure_daemon_announced() -> Result<(), i32> {
    if spt_daemon::is_running() {
        return Ok(());
    }
    eprintln!("spt-core daemon is offline - coming online...");
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_FAIL: {e}");
        return Err(1);
    }
    let ident = match (
        spt_store::hostlabel::os_hostname(),
        spt_store::nodeid::load_or_create().ok().map(|i| i.public_key().to_hex()),
    ) {
        (Some(l), Some(hex)) => {
            format!("{l} ({}…)", spt_net::net::registry::key_prefix(&hex))
        }
        (None, Some(hex)) => format!("{}…", spt_net::net::registry::key_prefix(&hex)),
        _ => "this node".to_string(),
    };
    eprintln!("spt-core daemon: online - {ident}");
    // Attachment states: every held subnet is attached while the daemon is
    // up (CONTEXT §subnet attachment — detach verbs are deferred); a
    // net-less daemon (endpoint bind failed) serves nothing: no connection.
    let net_up = spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms())
        .ok()
        .and_then(|mut b| b.net_status().ok())
        .map(|s| s.enabled)
        .unwrap_or(true); // can't ask ⇒ assume serving rather than alarm
    let state = if net_up { "attached" } else { "no connection" };
    let rows: Vec<(String, &'static str)> =
        SubnetStore::load().subnets.iter().map(|s| (s.name.clone(), state)).collect();
    eprint!("{}", render_subnets_joined(&rows));
    Ok(())
}

/// `spt subnet [status [NAME] [--nodes]]` — the membership view (REQ-SUBNET-1,

+/// Render the coming-online banner's "spt subnets joined" section + the

+/// concluding divider: header, underline, one `NAME: <state>` row per held

+/// subnet (`(none)` when empty), divider. Pure for tests.

+fn render_subnets_joined(rows: &[(String, &'static str)]) -> String {

+    let mut out = String::from("spt subnets joined\n──────────────────\n");

+            out.push_str(&format!("  {name}: {state}\n"));

+    out.push_str("────────────────────────────────────────\n");

+/// Ensure the per-machine daemon is up for a daemon-dependent command —

+/// silent when it already runs; otherwise announce the transition on

+/// stderr (stdout stays payload-pure) and conclude with a divider before

+/// the command's own output (grill 2026-06-06). The membership-creating

+/// verbs call this because **membership implies reachability** (CONTEXT

+/// §subnet attachment): a sole seed-holder with no responder daemon is a

+/// contradiction of the subnet's purpose.

+fn ensure_daemon_announced() -> Result<(), i32> {

+    eprintln!("spt-core daemon is offline - coming online...");

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

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

+        spt_store::hostlabel::os_hostname(),

+        spt_store::nodeid::load_or_create().ok().map(|i| i.public_key().to_hex()),

+            format!("{l} ({}…)", spt_net::net::registry::key_prefix(&hex))

+        (None, Some(hex)) => format!("{}…", spt_net::net::registry::key_prefix(&hex)),

+    eprintln!("spt-core daemon: online - {ident}");

+    // Attachment states: every held subnet is attached while the daemon is

+    // up (CONTEXT §subnet attachment — detach verbs are deferred); a

+    // net-less daemon (endpoint bind failed) serves nothing: no connection.

+    let net_up = spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms())

+        .and_then(|mut b| b.net_status().ok())

+        .unwrap_or(true); // can't ask ⇒ assume serving rather than alarm

+    let state = if net_up { "attached" } else { "no connection" };

+    let rows: Vec<(String, &'static str)> =

+        SubnetStore::load().subnets.iter().map(|s| (s.name.clone(), state)).collect();

+    eprint!("{}", render_subnets_joined(&rows));

/// M7 plan decisions 5 + 6). Read-only and ungated: prints membership shape,


fn cmd_subnet_create(name: String) -> i32 {
    let mut store = SubnetStore::load();
    let resolved = resolve(&mut store, None, Some(name));
    let result = decide_create(resolved, now_unix(), elevation::current());
    finish_show(result, &store)
}

fn cmd_subnet_create(name: String) -> i32 {
    let mut store = SubnetStore::load();
    let resolved = resolve(&mut store, None, Some(name));
    let result = decide_create(resolved, now_unix(), elevation::current());
    if result.save {
        let code = save_minted(&store);
        if code != 0 {
            return code;
        }
    }
    // Membership implies reachability (CONTEXT §subnet attachment): the
    // sole seed-holder must be answering joins by the time the join hint
    // below prints. After the persist (a refused create starts nothing),
    // before the payload (the banner concludes with its divider first).
    // [impl->REQ-SUBNET-2]
    if result.code == 0 {
        if let Err(code) = ensure_daemon_announced() {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    result.code
}


fn cmd_subnet_create(name: String) -> i32 {
    let mut store = SubnetStore::load();
    let resolved = resolve(&mut store, None, Some(name));
    let result = decide_create(resolved, now_unix(), elevation::current());
    finish_show(result, &store)
}

fn cmd_subnet_create(name: String) -> i32 {
    let mut store = SubnetStore::load();
    let resolved = resolve(&mut store, None, Some(name));
    let result = decide_create(resolved, now_unix(), elevation::current());
    if result.save {
        let code = save_minted(&store);
        if code != 0 {
            return code;
        }
    }
    // Membership implies reachability (CONTEXT §subnet attachment): the
    // sole seed-holder must be answering joins by the time the join hint
    // below prints. After the persist (a refused create starts nothing),
    // before the payload (the banner concludes with its divider first).
    // [impl->REQ-SUBNET-2]
    if result.code == 0 {
        if let Err(code) = ensure_daemon_announced() {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    result.code
}

+        let code = save_minted(&store);

+    // Membership implies reachability (CONTEXT §subnet attachment): the

+    // sole seed-holder must be answering joins by the time the join hint

+    // below prints. After the persist (a refused create starts nothing),

+    // before the payload (the banner concludes with its divider first).

+        if let Err(code) = ensure_daemon_announced() {

+        let _ = std::io::stdout().flush();

/// One subnet's status-view row: membership shape only — never seeds, epochs,


    if spt_daemon::ensure_running().is_err() {
        eprintln!("DAEMON_START_FAIL: the join ceremony runs on the daemon's endpoint");
        return 1;
    }
    let mut brain =

    if let Err(code) = ensure_daemon_announced() {
        return code;
    }
    let mut brain =


    if spt_daemon::ensure_running().is_err() {
        eprintln!("DAEMON_START_FAIL: the join ceremony runs on the daemon's endpoint");
        return 1;
    }
    let mut brain =

    if let Err(code) = ensure_daemon_announced() {
        return code;
    }
    let mut brain =

-    if spt_daemon::ensure_running().is_err() {

-        eprintln!("DAEMON_START_FAIL: the join ceremony runs on the daemon's endpoint");

+    if let Err(code) = ensure_daemon_announced() {

match spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms()) {


    // The digest lives in the daemon; bring it up if absent (best-effort).
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_WARN: {e}");
    }

    // The digest lives in the daemon; bring it up if absent (best-effort —
    // a start failure still tries the socket; announced when it transitions).
    let _ = ensure_daemon_announced();


    // The digest lives in the daemon; bring it up if absent (best-effort).
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_WARN: {e}");
    }

    // The digest lives in the daemon; bring it up if absent (best-effort —
    // a start failure still tries the socket; announced when it transitions).
    let _ = ensure_daemon_announced();

eprintln!("CROSS_NODE_M4:{id} — qualified [subnet:]id@node addressing is M4, not yet supported");

-    // The digest lives in the daemon; bring it up if absent (best-effort).

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

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

+    // The digest lives in the daemon; bring it up if absent (best-effort —

+    // a start failure still tries the socket; announced when it transitions).

let name = spt_daemon::digest_socket_name();


fn cmd_update_apply() -> i32 {
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_FAIL: {e}");
        return 1;
    }

fn cmd_update_apply() -> i32 {
    if let Err(code) = ensure_daemon_announced() {
        return code;
    }


fn cmd_update_apply() -> i32 {
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_FAIL: {e}");
        return 1;
    }

fn cmd_update_apply() -> i32 {
    if let Err(code) = ensure_daemon_announced() {
        return code;
    }

-        eprintln!("DAEMON_START_FAIL: {e}");

let exe = match std::env::current_exe() {


Unit test for the banner render + build:


// [unit->REQ-DOCS-6] how-to: bare lists every topic; each topic prints

    // [unit->REQ-DAEMON-3] [unit->REQ-SUBNET-2] the coming-online banner's
    // subnets-joined section: header + underline, one `NAME: state` row per
    // held subnet ((none) when empty), and it always concludes with the
    // divider that separates daemon chatter from the command's own output.
    #[test]
    fn subnets_joined_banner_renders_rows_and_divider() {
        let rows = vec![("BIGNET".to_string(), "attached"), ("work".to_string(), "no connection")];
        let out = render_subnets_joined(&rows);
        assert!(out.starts_with("spt subnets joined\n──────"), "header + underline: {out}");
        assert!(out.contains("  BIGNET: attached\n"));
        assert!(out.contains("  work: no connection\n"));
        assert!(out.ends_with("────────────────────────────────────────\n"), "concluding divider");

        let none = render_subnets_joined(&[]);
        assert!(none.contains("(none)"));
        assert!(none.ends_with("────────────────────────────────────────\n"));
    }

    // [unit->REQ-DOCS-6] how-to: bare lists every topic; each topic prints


    // [unit->REQ-DAEMON-3] [unit->REQ-SUBNET-2] the coming-online banner's
    // subnets-joined section: header + underline, one `NAME: state` row per
    // held subnet ((none) when empty), and it always concludes with the
    // divider that separates daemon chatter from the command's own output.
    #[test]
    fn subnets_joined_banner_renders_rows_and_divider() {
        let rows = vec![("BIGNET".to_string(), "attached"), ("work".to_string(), "no connection")];
        let out = render_subnets_joined(&rows);
        assert!(out.starts_with("spt subnets joined\n──────"), "header + underline: {out}");
        assert!(out.contains("  BIGNET: attached\n"));
        assert!(out.contains("  work: no connection\n"));
        assert!(out.ends_with("────────────────────────────────────────\n"), "concluding divider");

        let none = render_subnets_joined(&[]);
        assert!(none.contains("(none)"));
        assert!(none.ends_with("────────────────────────────────────────\n"));
    }

    // [unit->REQ-DOCS-6] how-to: bare lists every topic; each topic prints

+    // [unit->REQ-DAEMON-3] [unit->REQ-SUBNET-2] the coming-online banner's

+    // subnets-joined section: header + underline, one `NAME: state` row per

+    // held subnet ((none) when empty), and it always concludes with the

+    // divider that separates daemon chatter from the command's own output.

+    fn subnets_joined_banner_renders_rows_and_divider() {

+        let rows = vec![("BIGNET".to_string(), "attached"), ("work".to_string(), "no connection")];

+        let out = render_subnets_joined(&rows);

+        assert!(out.starts_with("spt subnets joined\n──────"), "header + underline: {out}");

+        assert!(out.contains("  BIGNET: attached\n"));

+        assert!(out.contains("  work: no connection\n"));

+        assert!(out.ends_with("────────────────────────────────────────\n"), "concluding divider");

+        let none = render_subnets_joined(&[]);

+        assert!(none.contains("(none)"));

+        assert!(none.ends_with("────────────────────────────────────────\n"));

// text naming the commands the guidance relies on; unknown is usage error.


Bash

cargo test -p spt --bin spt 2>&1 | grep -E "test result|error\[|warning: unused|FAILED" | head -5

Build + unit tests


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


H=$(mktemp -d); SPT_HOME=$H ./target/debug/spt.exe digest nothing 2>&1 | head -12; echo "exit=$?"; PID=$(cat $H/daemon.pid 2>/dev/null || find $H -name "*.pid" -exec cat {} \; 2>/dev/null | head -1); echo "daemon pid: $PID"; [ -n "$PID" ] && taskkill //PID $PID //F 2>&1 | head -1

Smoke banner via digest on fresh home, then kill spawned daemon


NO_DIGEST:nothing has no live activity buffer (not a daemon-hosted session?)
exit=0
daemon pid: 71224
SUCCESS: The process with PID 71224 has been terminated.


Stale binary (`cargo test` builds the test harness, not the bin). Rebuild, re-smoke:


cargo build -p spt 2>&1 | tail -1; H=$(mktemp -d); SPT_HOME=$H ./target/debug/spt.exe digest nothing 2>&1 | head -14; echo "exit=$?"; PID=$(cat $H/daemon.pid 2>/dev/null); [ -n "$PID" ] && taskkill //PID $PID //F 2>&1 | head -1

Rebuild + smoke banner


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 18.78s
spt-core daemon is offline - coming online...
spt-core daemon: online - HFENDULEAM (1488f75e…)
spt subnets joined
──────────────────
  (none)
────────────────────────────────────────
NO_DIGEST:nothing has no live activity buffer (not a daemon-hosted session?)
exit=0
SUCCESS: The process with PID 72196 has been terminated.


cargo test -p spt 2>&1 | grep -E "test result" | awk '{s+=$4; f+=$6} END {print "spt passed:"s" failed:"f}'; cargo run -p xtask -- check 2>&1 | tail -1; traceable-reqs check >/dev/null 2>&1; echo "reqs=$?"

Full spt suite + gates


spt passed:98 failed:0
xtask check: OK
reqs=0


git add -A && git commit -m "$(cat <<'EOF'
feat(cli): membership implies reachability — daemon activation + coming-online banner

Found by the first M7 acceptance run: `spt subnet create` minted the seed
and exited — no daemon, so no pairing responder and no meet listener; the
sole seed-holder was dark the moment the join hint printed, and a joiner
got NO_SEED_HOLDER. Decision 8's "responder always-on in every member
daemon" silently assumed a daemon exists; nothing made one exist.

Ratified (grill 2026-06-06, CONTEXT §subnet attachment):

- Membership implies reachability: the membership-creating verbs ensure
  the daemon. `subnet create` activates it after the persist (a refused
  mint starts nothing) and before the payload, so the printed join hint
  is true the moment it lands; `join` already needed it.
- Coming-online banner (stderr — stdout stays payload-pure): offline
  notice, online line with the node's label + key prefix, a
  "spt subnets joined" section (NAME: attached per held subnet; a
  net-less daemon renders "no connection"; (none) when empty), and a
  concluding divider before the command's own output. Silent when the
  daemon already runs. digest/update-apply now share the same helper.
- New glossary terms (CONTEXT.md): attached/detached = deliberate
  per-subnet serve-state (verbs deferred — DEFERRED.md entry with
  detach/attach [--auto], leave, disband), "no connection" = degraded
  net-less state, never conflated; "endpoint description" = the CLI term
  for the resource blurb.
- DEFERRED.md also records the ratified M8 CLI-nounification shape
  (endpoint namespace, hot path stays flat, endpoint-scoped access,
  merged endpoint list with pinned self, subnet notify, daemon noun) —
  M8 work, minted there so M7 acceptance stays a fixed target.

spt suite 98 green (banner render unit-tested); xtask check +
traceable-reqs check green. Smoke-verified on a fresh home: banner +
divider + command output exactly per spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)" && git log --oneline -1

Commit daemon activation + banner


warning: in the working copy of 'CONTEXT.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/DEFERRED.md', LF will be replaced by CRLF the next time Git touches it
[dev-freeform eeb6f01] feat(cli): membership implies reachability — daemon activation + coming-online banner
 3 files changed, 115 insertions(+), 11 deletions(-)
eeb6f01 feat(cli): membership implies reachability — daemon activation + coming-online banner


**Grill complete.** Session yield:

- Gap (create-then-dark) → fixed: `create` activates the daemon post-persist; banner per your spec, verified live
- Glossary: attached/detached + no-connection + endpoint description → CONTEXT.md
- Deferred registry: detach/attach `[--auto]`, leave, disband(?), daemon noun, full M8 nounification shape → DEFERRED.md
- Collision caught: `spt shutdown` stays agent-verb; daemon gets `spt daemon stop` (M8)
- Commit eeb6f01, all gates green

**Before you retry acceptance — two things:**

1. Deployed binaries are now stale (pre-banner). Want me to redeploy both machines? (rebuild release here → `spt-core\bin`; ssh gravity: pull + rebuild + `~/.local/bin`)
2. Your BIGNET run hit the **real** home (`HOME_ADOPTED:alice` = leftover perch; BIGNET seed now persisted there). For a clean "fresh two-machine" acceptance: wipe `%LOCALAPPDATA%\spt-core` first, or accept BIGNET as the acceptance subnet and carry on from here. Your call.


before we publish this update, can you please make UX changes to the `subnet create` output: "HOME_ADOPTED:alice" --> "NOW THE SYNC HOME FOR: alice", "SUBNET_CREATED:BIGNET" --> "SUBNET CREATED: BIGNET" ++ move "SYNC HOME" line to be **after** "SUBNET CREATED" line. -also need to have it show the TOTP expiration time somewhere, in local timezone. Revision i.e. "Run [...] on other machines to have them join." // "Code expires at 12:34. To pair again, scan QR with your auth app or run `spt subnet show-code`" (exp time floored to minute). note the command change `show-totp`-->`show-code`. roll this into M7, then redeploy both machines. any challenges/expansions/sharpening?


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


Challenges + sharpening — four points, then I build:

**1. Floored-minute math trap.** Code windows end at :00/:30. Naive floor can print a time already past (window ends 12:34:30, floored "12:34" — read at 12:34:40, looks expired-before-printed... or worse, still-valid-but-says-past). Fix that's still minute-clean: the responder accepts ±1 step, so the code is *actually* honored until `window_end + 30s` — which is always exactly :00 or :30. Floor **that**: ends 12:34:30 → honored to 12:35:00 → "expires at 12:35" (exact); ends 12:35:00 → honored to 12:35:30 → "12:35" (conservative). Every printed time is a true promise. **Recommend this.**

**2. `show-code` must earn its sentence.** The expiry line says "scan QR … or run `spt subnet show-code`" — but today show-totp prints code+URI, *no QR*, and your terminal QR scrolled away. Recommend show-code = code + URI + **QR** + its own expiry line — also closes ADR-0005's "recovery = re-provision the QR from any still-trusted node" promise, which today no command actually fulfills. Rename clean, no alias (zero users).

**3. Register shift acknowledged.** `SUBNET CREATED: BIGNET` / `NOW THE SYNC HOME FOR: alice` break the greppable `TAG:` convention — fine for ceremony output, but I'll keep hot-path tags (`SENT:`/`QUEUED:`/`READY:`/`JOINED:`) untouched, and apply the new sync-home wording in **join's** adoption line too (same event, must read the same). Confirm `JOINED:BIGNET` stays a tag, or want join's ceremony line humanized as well?

**4. New dep.** Local-timezone formatting needs `chrono` (std has no local tz) — CLI crate only, features trimmed. Accept.

Sweep scope (included): hint footer, quickstart/networking/reference docs, ADR amendment text, REQ-SUBNET-1 title annotation, reference regen.

Rule on 1/2/3-join-line/4 — then I implement, commit into M7, redeploy both machines.


before we publish this update, can you please make UX changes to the `subnet create` output: "HOME_ADOPTED:alice" --> "NOW THE SYNC HOME FOR: alice", "SUBNET_CREATED:BIGNET" --> "SUBNET CREATED: BIGNE…


1. agree with rec 2. agree 3. also humanize join's ceremony line. anything barred behind admin is unlikely to be grepped anyway. 4. accept --- proceed


(so "JOINED SUBNET: BIGNET")


Edit

# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
# otpauth:// URI as unicode half-blocks. default-features off drops the
# image/svg renderers — the unicode renderer is all the CLI needs.
qrcode = { version = "0.14", default-features = false }

# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
# otpauth:// URI as unicode half-blocks. default-features off drops the
# image/svg renderers — the unicode renderer is all the CLI needs.
qrcode = { version = "0.14", default-features = false }
# Local-timezone rendering of the pairing code's expiry instant (std has no
# local tz). Clock feature only — no serde, no wasm.
chrono = { version = "0.4", default-features = false, features = ["clock"] }


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

# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
# otpauth:// URI as unicode half-blocks. default-features off drops the
# image/svg renderers — the unicode renderer is all the CLI needs.
qrcode = { version = "0.14", default-features = false }
# Local-timezone rendering of the pairing code's expiry instant (std has no
# local tz). Clock feature only — no serde, no wasm.
chrono = { version = "0.4", default-features = false, features = ["clock"] }

[package]
name = "spt"
version.workspace = true
edition = "2021"
publish = false
description = "spt-core CLI: the single `spt` binary. Thin command layer over spt-msg (send/ring/ready/list/stop/whoami) + the harness-contract `api` surface (spt-runtime)."

[[bin]]
name = "spt"
path = "src/main.rs"

[dependencies]
spt-proto = { path = "../spt-proto" }
spt-store = { path = "../spt-store" }
spt-msg = { path = "../spt-msg" }
spt-runtime = { path = "../spt-runtime" }
spt-live = { path = "../spt-live" }
spt-daemon = { path = "../spt-daemon" }
# The reference binary ships the WAN transport on by default (ADR-0002). D2g reuses its
# RFC-6238 TotpSeed (spt_net::net::pairing::totp) to render a subnet's local pairing
# code — reused, not reimplemented, so the CLI code matches the ceremony's exactly.
spt-net = { path = "../spt-net" }
# `wrap_help`: wrap long help text to the terminal width with a hanging indent
# (without it clap emits unwrapped lines and the terminal hard-wraps to col 0).
clap = { version = "4", features = ["derive", "wrap_help"] }
# The WAN send leg (D9-1-4) serializes the dial address + reads registry
# snapshots - same pinned shape as spt-daemon's.
serde_json = { version = "1", features = ["preserve_order"] }
# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
# otpauth:// URI as unicode half-blocks. default-features off drops the
# image/svg renderers — the unicode renderer is all the CLI needs.
qrcode = { version = "0.14", default-features = false }

# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the
# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing
# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
    "Win32_Foundation",
    "Win32_Security",
    "Win32_System_Threading",
] }

[dev-dependencies]
tempfile = "3"
toml = "0.8"
# install_e2e stages the built binary as a release asset (SHA256SUMS) for the
# D2 installer-script rung — same sha2 the workspace already carries.
sha2 = "0.10"

+# Local-timezone rendering of the pairing code's expiry instant (std has no

+# local tz). Clock feature only — no serde, no wasm.

+chrono = { version = "0.4", default-features = false, features = ["clock"] }


Prompt: (so "JOINED SUBNET: BIGNET")


Noted — `JOINED SUBNET: BIGNET`. Continuing:


    /// 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 name the node's sole subnet is used; if it holds
    /// several, the name is required (never guessed).
    ShowTotp {
        /// Which subnet's code to show. Required only when the node holds several.
        name: Option<String>,
    },

    /// Show a subnet's current 6-digit pairing code (+ URI and QR).
    ///
    /// The re-provisioning surface: prints the same joining material as
    /// `create` — current code, `otpauth://` URI, terminal QR, expiry.
    /// Gated behind OS elevation (or read the code from your authenticator
    /// app). With no name the node's sole subnet is used; if it holds
    /// several, the name is required (never guessed).
    ShowCode {
        /// Which subnet's code to show. Required only when the node holds several.
        name: Option<String>,
    },


    /// 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 name the node's sole subnet is used; if it holds
    /// several, the name is required (never guessed).
    ShowTotp {
        /// Which subnet's code to show. Required only when the node holds several.
        name: Option<String>,
    },

    /// Show a subnet's current 6-digit pairing code (+ URI and QR).
    ///
    /// The re-provisioning surface: prints the same joining material as
    /// `create` — current code, `otpauth://` URI, terminal QR, expiry.
    /// Gated behind OS elevation (or read the code from your authenticator
    /// app). With no name the node's sole subnet is used; if it holds
    /// several, the name is required (never guessed).
    ShowCode {
        /// Which subnet's code to show. Required only when the node holds several.
        name: Option<String>,
    },

-    /// Show a subnet's current 6-digit pairing code + `otpauth://` URI.

+    /// Show a subnet's current 6-digit pairing code (+ URI and QR).

+    /// The re-provisioning surface: prints the same joining material as

+    /// `create` — current code, `otpauth://` URI, terminal QR, expiry.


                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowTotp { name } => cmd_subnet_show_totp(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),

                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),


                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowTotp { name } => cmd_subnet_show_totp(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),

                SubnetCmd::Status { name, nodes } => cmd_subnet_status(name, nodes),
                SubnetCmd::Create { name } => cmd_subnet_create(name),
                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),
                SubnetCmd::Join { name, code } => cmd_subnet_join(name, code),

match action.unwrap_or(SubnetCmd::Status { name: None, nodes: false }) {

-                SubnetCmd::ShowTotp { name } => cmd_subnet_show_totp(name),

+                SubnetCmd::ShowCode { name } => cmd_subnet_show_code(name),


/// Decide the rendered outcome from a resolution + the clock + the elevation level.
/// The elevation gate is applied LAST: a resolved subnet still falls back (and leaks
/// no code) unless the process is confirmed elevated.
// [impl->REQ-PAIR-3] [impl->REQ-PAIR-6]
fn decide_show_totp(resolved: Resolved, now: u64, elevation: Elevation) -> ShowResult {
    match resolved {
        Resolved::Subnet { name, seed, minted } => match elevation::gate_for(elevation) {
            Gate::Show => {
                let totp = TotpSeed::from_bytes(seed);
                // The URI carries the base32 secret — importable into an authenticator
                // app; QR *rendering* is GUI/later, so we print the URI text only.
                let out = format!("{}\n{}\n", totp.code_at(now), totp.otpauth_uri(ISSUER, &name));
                ShowResult { code: 0, out: Some(out), msg: format!("TOTP:{name} (elevated)"), save: minted }
            }

/// The wall-clock instant the CURRENT code stops being honored, rendered as
/// local "HH:MM". The responder accepts ±1 step, so a code minted in this
/// window is good until the NEXT window's end — always a :00/:30 boundary,
/// so dropping the seconds (the floor) never promises time that was already
/// gone when the line printed (grill 2026-06-06 point 1).
fn code_expiry_local_hm(now: u64) -> String {
    use chrono::TimeZone;
    let honored_until = (now / PERIOD_SECS + 2) * PERIOD_SECS;
    match chrono::Local.timestamp_opt(honored_until as i64, 0) {
        chrono::LocalResult::Single(dt) => dt.format("%H:%M").to_string(),
        _ => "??:??".to_string(), // unrepresentable clock — never panic a UX line
    }
}

/// Decide the rendered outcome from a resolution + the clock + the elevation level.
/// The elevation gate is applied LAST: a resolved subnet still falls back (and leaks
/// no code) unless the process is confirmed elevated. The elevated output is the
/// full re-provisioning material (grill 2026-06-06 point 2 — this is how ADR-0005's
/// "re-provision the QR from any still-trusted node" is actually fulfilled): code,
/// `otpauth://` URI, terminal QR, expiry.
// [impl->REQ-PAIR-3] [impl->REQ-PAIR-6]
fn decide_show_code(resolved: Resolved, now: u64, elevation: Elevation) -> ShowResult {
    match resolved {
        Resolved::Subnet { name, seed, minted } => match elevation::gate_for(elevation) {
            Gate::Show => {
                let totp = TotpSeed::from_bytes(seed);
                let uri = totp.otpauth_uri(ISSUER, &name);
                let mut out = format!("{}\n{uri}\n", totp.code_at(now));
                if let Some(qr) = qr_unicode(&uri) {
                    out.push_str(&qr);
                    out.push('\n');
                }
                out.push_str(&format!(
                    "Code expires at {}. Codes rotate every {PERIOD_SECS}s — re-run for the \
                     next one, or scan the QR into your auth app.\n",
                    code_expiry_local_hm(now)
                ));
                ShowResult { code: 0, out: Some(out), msg: format!("TOTP:{name} (elevated)"), save: minted }
            }


/// Decide the rendered outcome from a resolution + the clock + the elevation level.
/// The elevation gate is applied LAST: a resolved subnet still falls back (and leaks
/// no code) unless the process is confirmed elevated.
// [impl->REQ-PAIR-3] [impl->REQ-PAIR-6]
fn decide_show_totp(resolved: Resolved, now: u64, elevation: Elevation) -> ShowResult {
    match resolved {
        Resolved::Subnet { name, seed, minted } => match elevation::gate_for(elevation) {
            Gate::Show => {
                let totp = TotpSeed::from_bytes(seed);
                // The URI carries the base32 secret — importable into an authenticator
                // app; QR *rendering* is GUI/later, so we print the URI text only.
                let out = format!("{}\n{}\n", totp.code_at(now), totp.otpauth_uri(ISSUER, &name));
                ShowResult { code: 0, out: Some(out), msg: format!("TOTP:{name} (elevated)"), save: minted }
            }

/// The wall-clock instant the CURRENT code stops being honored, rendered as
/// local "HH:MM". The responder accepts ±1 step, so a code minted in this
/// window is good until the NEXT window's end — always a :00/:30 boundary,
/// so dropping the seconds (the floor) never promises time that was already
/// gone when the line printed (grill 2026-06-06 point 1).
fn code_expiry_local_hm(now: u64) -> String {
    use chrono::TimeZone;
    let honored_until = (now / PERIOD_SECS + 2) * PERIOD_SECS;
    match chrono::Local.timestamp_opt(honored_until as i64, 0) {
        chrono::LocalResult::Single(dt) => dt.format("%H:%M").to_string(),
        _ => "??:??".to_string(), // unrepresentable clock — never panic a UX line
    }
}

/// Decide the rendered outcome from a resolution + the clock + the elevation level.
/// The elevation gate is applied LAST: a resolved subnet still falls back (and leaks
/// no code) unless the process is confirmed elevated. The elevated output is the
/// full re-provisioning material (grill 2026-06-06 point 2 — this is how ADR-0005's
/// "re-provision the QR from any still-trusted node" is actually fulfilled): code,
/// `otpauth://` URI, terminal QR, expiry.
// [impl->REQ-PAIR-3] [impl->REQ-PAIR-6]
fn decide_show_code(resolved: Resolved, now: u64, elevation: Elevation) -> ShowResult {
    match resolved {
        Resolved::Subnet { name, seed, minted } => match elevation::gate_for(elevation) {
            Gate::Show => {
                let totp = TotpSeed::from_bytes(seed);
                let uri = totp.otpauth_uri(ISSUER, &name);
                let mut out = format!("{}\n{uri}\n", totp.code_at(now));
                if let Some(qr) = qr_unicode(&uri) {
                    out.push_str(&qr);
                    out.push('\n');
                }
                out.push_str(&format!(
                    "Code expires at {}. Codes rotate every {PERIOD_SECS}s — re-run for the \
                     next one, or scan the QR into your auth app.\n",
                    code_expiry_local_hm(now)
                ));
                ShowResult { code: 0, out: Some(out), msg: format!("TOTP:{name} (elevated)"), save: minted }
            }

+/// The wall-clock instant the CURRENT code stops being honored, rendered as

+/// local "HH:MM". The responder accepts ±1 step, so a code minted in this

+/// window is good until the NEXT window's end — always a :00/:30 boundary,

+/// so dropping the seconds (the floor) never promises time that was already

+/// gone when the line printed (grill 2026-06-06 point 1).

+fn code_expiry_local_hm(now: u64) -> String {

+    let honored_until = (now / PERIOD_SECS + 2) * PERIOD_SECS;

+    match chrono::Local.timestamp_opt(honored_until as i64, 0) {

+        chrono::LocalResult::Single(dt) => dt.format("%H:%M").to_string(),

+        _ => "??:??".to_string(), // unrepresentable clock — never panic a UX line

-/// no code) unless the process is confirmed elevated.

+/// no code) unless the process is confirmed elevated. The elevated output is the

+/// full re-provisioning material (grill 2026-06-06 point 2 — this is how ADR-0005's

+/// "re-provision the QR from any still-trusted node" is actually fulfilled): code,

+/// `otpauth://` URI, terminal QR, expiry.

-fn decide_show_totp(resolved: Resolved, now: u64, elevation: Elevation) -> ShowResult {

+fn decide_show_code(resolved: Resolved, now: u64, elevation: Elevation) -> ShowResult {

-                // The URI carries the base32 secret — importable into an authenticator

-                // app; QR *rendering* is GUI/later, so we print the URI text only.

-                let out = format!("{}\n{}\n", totp.code_at(now), totp.otpauth_uri(ISSUER, &name));

+                let uri = totp.otpauth_uri(ISSUER, &name);

+                let mut out = format!("{}\n{uri}\n", totp.code_at(now));

+                if let Some(qr) = qr_unicode(&uri) {

+                    "Code expires at {}. Codes rotate every {PERIOD_SECS}s — re-run for the \

+                     next one, or scan the QR into your auth app.\n",

+                    code_expiry_local_hm(now)

// Refused fetch: no code, and we do NOT persist a `--create-new` mint —


Bash

grep -n "decide_show_totp\|show-totp\|show_totp\|ShowTotp" crates/spt/src/cli.rs | head -30

Find remaining rename sites


33:/// `spt subnet show-totp`/`create` refused the fetch for lack of OS elevation. Distinct
112:    /// Subnet membership: status, create, show-totp.
554:        /// The current 6-digit code (`spt subnet show-totp` on a member
1047:/// force qualification (no guessing — the D3c/`show-totp` stance).
1472:/// What `show-totp` resolved its arguments to against the node's subnet store — the
1491:/// The rendered outcome of `show-totp`: an exit code, the optional stdout block (the
1502:/// Resolve `show-totp`/`create` arguments against the store (mutating it iff a create
1589:                "AMBIGUOUS_SUBNET: this node holds {} subnets ({}) — name one: `spt subnet show-totp <name>`",
1610:            msg: format!("SUBNET_EXISTS:{n} — already a member; `spt subnet show-totp {n}` shows its code"),
1623:/// Same gate-last shape as [`decide_show_totp`] — a refused mint persists nothing and
1653:            // path needs confirmed privilege — same posture as show-totp).
1664:        other => decide_show_totp(other, now, elevation),
1708:/// `spt subnet show-totp [NAME]` — surface a subnet's current code off this node,
1709:/// behind the OS-elevation gate (REQ-PAIR-3/6; ported off `spt pair show-totp`, M7
1711:/// unit-tested [`resolve`]/[`decide_show_totp`] core.
1713:fn cmd_subnet_show_totp(name: Option<String>) -> i32 {
1716:    let result = decide_show_totp(resolved, now_unix(), elevation::current());
1722:/// `show-totp --create-new` un-buried, M7 plan decisions 3 + 11). Elevation-gated:
1819:     hint: `spt subnet show-totp [name]` shows a subnet's joining code (elevated)\n";
2181:                     machine (`spt subnet show-totp`) and retry"
4565:    // bare (= status), status [NAME], create <NAME>, show-totp [NAME] — all
4588:            subnet_cmd(&["spt", "subnet", "show-totp"]),
4589:            Some(SubnetCmd::ShowTotp { name: None })
4592:            subnet_cmd(&["spt", "subnet", "show-totp", "home"]),
4593:            Some(SubnetCmd::ShowTotp { name: Some(n) }) if n == "home"
4628:        assert!(parse(&["spt", "pair", "show-totp"]).is_err());
4652:    // show-totp) — the create half of the membership-mutation gates.
4684:        assert!(empty.contains("show-totp"));
4845:        let r = decide_show_totp(resolve(&mut store, None, None), now, Elevation::Elevated);
4859:        let amb = decide_show_totp(resolve(&mut store, None, None), 0, Elevation::Elevated);


sed -i 's/decide_show_totp/decide_show_code/g; s/cmd_subnet_show_totp/cmd_subnet_show_code/g; s/`spt subnet show-totp <name>`/`spt subnet show-code <name>`/; s/`spt subnet show-totp {n}` shows its code/`spt subnet show-code {n}` shows its code/; s/`spt subnet show-totp`) and retry/`spt subnet show-code`) and retry/' crates/spt/src/cli.rs && grep -c "decide_show_code" crates/spt/src/cli.rs

Bulk-rename decide/cmd fns + messages


sed -i 's/    \/\/\/ Subnet membership: status, create, show-totp./    \/\/\/ Subnet membership: status, create, show-code./; s/`spt subnet show-totp` on a member/`spt subnet show-code` on a member/; s/`spt subnet show-totp \[NAME\]` — surface/`spt subnet show-code [NAME]` — surface/; s/hint: `spt subnet show-totp \[name\]` shows a subnet.s joining code (elevated)/hint: `spt subnet show-code [name]` shows a subnet'"'"'s joining code (elevated)/' crates/spt/src/cli.rs; sed -i 's/Some(SubnetCmd::ShowTotp { name: None })/Some(SubnetCmd::ShowCode { name: None })/; s/Some(SubnetCmd::ShowTotp { name: Some(n) }) if n == "home"/Some(SubnetCmd::ShowCode { name: Some(n) }) if n == "home"/' crates/spt/src/cli.rs; sed -i 's/subnet_cmd(&\["spt", "subnet", "show-totp"\])/subnet_cmd(\&["spt", "subnet", "show-code"])/; s/subnet_cmd(&\["spt", "subnet", "show-totp", "home"\])/subnet_cmd(\&["spt", "subnet", "show-code", "home"])/' crates/spt/src/cli.rs; grep -n "show-totp\|ShowTotp" crates/spt/src/cli.rs | head

Sweep remaining show-totp tokens in cli.rs


33:/// `spt subnet show-totp`/`create` refused the fetch for lack of OS elevation. Distinct
1047:/// force qualification (no guessing — the D3c/`show-totp` stance).
1472:/// What `show-totp` resolved its arguments to against the node's subnet store — the
1491:/// The rendered outcome of `show-totp`: an exit code, the optional stdout block (the
1502:/// Resolve `show-totp`/`create` arguments against the store (mutating it iff a create
1653:            // path needs confirmed privilege — same posture as show-totp).
1709:/// behind the OS-elevation gate (REQ-PAIR-3/6; ported off `spt pair show-totp`, M7
1722:/// `show-totp --create-new` un-buried, M7 plan decisions 3 + 11). Elevation-gated:
4565:    // bare (= status), status [NAME], create <NAME>, show-totp [NAME] — all
4628:        assert!(parse(&["spt", "pair", "show-totp"]).is_err());


perl -pi -e 's{`spt subnet show-totp`/`create` refused}{`spt subnet show-code`/`create` refused}; s{the D3c/`show-totp` stance}{the D3c/`show-code` stance}; s{What `show-totp` resolved}{What `show-code` resolved}; s{The rendered outcome of `show-totp`}{The rendered outcome of `show-code`}; s{Resolve `show-totp`/`create` arguments}{Resolve `show-code`/`create` arguments}; s{same posture as show-totp\)}{same posture as show-code)}; s{ported off `spt pair show-totp`, M7}{ported off `spt pair show-totp` (renamed show-code), M7}; s{`show-totp --create-new` un-buried}{`show-totp --create-new` un-buried (now show-code)}; s{create <NAME>, show-totp \[NAME\]}{create <NAME>, show-code [NAME]}' crates/spt/src/cli.rs && grep -cn "show-totp" crates/spt/src/cli.rs

Finish comment sweeps (historical refs annotated, not erased)


grep -n "show-totp" crates/spt/src/cli.rs

Inspect remaining 5


1709:/// behind the OS-elevation gate (REQ-PAIR-3/6; ported off `spt pair show-totp` (renamed show-code), M7
1722:/// `show-totp --create-new` un-buried (now show-code), M7 plan decisions 3 + 11). Elevation-gated:
4628:        assert!(parse(&["spt", "pair", "show-totp"]).is_err());
4652:    // show-totp) — the create half of the membership-mutation gates.
4684:        assert!(empty.contains("show-totp"));


sed -i '4684s/show-totp/show-code/' crates/spt/src/cli.rs && sed -n 4684p crates/spt/src/cli.rs

Fix footer hint assertion


assert!(empty.contains("show-code"));


Now create's output block — new hint wording, expiry line, humanized msg, sync-home ordering:


Edit

                let totp = TotpSeed::from_bytes(seed);
                let uri = totp.otpauth_uri(ISSUER, &name);
                let mut out = format!("{}\n{}\n", totp.code_at(now), uri);
                // Terminal QR of the provisioning URI — scan it into an
                // authenticator app from the other machine's side of the desk.
                if let Some(qr) = qr_unicode(&uri) {
                    out.push_str(&qr);
                    out.push('\n');
                }
                out.push_str(&format!(
                    "Run `spt subnet join {name}` on the other machine to pair it in.\n"
                ));
                ShowResult {
                    code: 0,
                    out: Some(out),
                    msg: format!("SUBNET_CREATED:{name} (this node is the sole seed-holder)"),
                    save: minted,
                }

                let totp = TotpSeed::from_bytes(seed);
                let uri = totp.otpauth_uri(ISSUER, &name);
                let mut out = format!("{}\n{}\n", totp.code_at(now), uri);
                // Terminal QR of the provisioning URI — scan it into an
                // authenticator app from the other machine's side of the desk.
                if let Some(qr) = qr_unicode(&uri) {
                    out.push_str(&qr);
                    out.push('\n');
                }
                out.push_str(&format!(
                    "Run `spt subnet join {name}` on other machines to have them join.\n\
                     Code expires at {}. To pair again, scan the QR with your auth app \
                     or run `spt subnet show-code`.\n",
                    code_expiry_local_hm(now)
                ));
                ShowResult {
                    code: 0,
                    out: Some(out),
                    msg: format!("SUBNET CREATED: {name} (this node is the sole seed-holder)"),
                    save: minted,
                }


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

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