# M7 — Subnet & quickstart UX

> **✅ M7 COMPLETE (2026-06-06).** D1–D5 delivered; acceptance ran on REAL
> hardware: fresh two-machine setup via `subnet create` + the user's own
> fully-guided `subnet join` (no test harness, elevation prompts honored),
> `subnet status --nodes` labeled+online on both machines, cross-machine
> `SENT(WAN)` proven in both directions (HFENDULEAM ↔ gravity-linux), and the
> rig's product-surface join rung carries the pairing `int` evidence. The
> acceptance run surfaced two publish-blocking field bugs, both fixed before
> closeout: elevated daemon spawn (KNOWN-HAZARDS 5.7,
> REQ-HAZARD-ELEVATED-DAEMON-SPAWN — de-elevation seam) and immortal ghost
> registry rows (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS —
> silent-peer eviction), plus the AMBIGUOUS-render copy-paste-targets fix.
> Session harvest (Linux elevation model, NTP-first clock, firewall
> registration, post-join address seeding, trust-prune verb, M8
> nounification spec) is logged in `docs/DEFERRED.md`.

Status was: planned (grill session 2026-06-05; all decisions user-ratified).
Goal: make subnet setup a first-class, guided, secure product surface, and make
the public quickstart honest about personas (humans pair machines; agents
exchange messages). Runs **before** spt-claude-code scoping — breaking CLI
changes are cheapest now, while no adapter depends on the surface.

## Root causes this milestone fixes

1. **The join ceremony has no product surface.** SPAKE2/TOTP pairing wire
   (`run_initiator`/`run_responder`, rate limiter, rendezvous) is rig-proven but
   invoked only from tests. The production daemon hosts no pairing responder;
   the CLI has no join command. Docs describe machinery a user cannot run
   (REQ-PAIR-4 registry note: "CLI naming prompt is later" — deferred, never
   delivered).
2. **Subnet creation hides inside a fetch flag** (`spt pair show-totp
   --create-new`) — verb-as-flag, undiscoverable.
3. **`spt poll` ≡ `spt ready`** (both dispatch `run_listen`; only `--once`
   differs) — duplicated verb, README/quickstart contradiction is the symptom.
4. **Quickstart casts the human as the agent** and never mentions subnets —
   the product's hallmark.
5. **Nodes render as bare pubkey hex** — no human label exists anywhere in the
   gossip.

## Ratified decisions

| # | Decision |
|---|----------|
| 1 | Full build (CLI reorg + join machinery + docs), one milestone. No deprecation shims — zero users. |
| 2 | `spt poll` removed. Surface: `spt ready <id>` (blocks) + `spt ready <id> --once` (drain-then-exit). `spt api poll` / `api worker-poll` untouched (machinery namespace). Amend REQ-MSG-2 wording. |
| 3 | New `spt subnet` noun namespace, subcommand verbs (not flags), positional subnet names (no `--subnet`/`--only` stutter): bare `spt subnet` = flagless status view · `spt subnet status [NAME] [--nodes]` · `spt subnet create <NAME>` · `spt subnet show-totp [NAME]` · `spt subnet join [NAME] [--code <CODE>]`. `spt pair` namespace deleted outright. `subnet` sits in the **User commands** help group. |
| 4 | Verb is **join**, not pair: a node *joins* a subnet; "pairing" names the node↔node ceremony (CONTEXT.md glossary updated). |
| 5 | Status view: subnet name, paired-node count, endpoint count ("ENDPOINTS", not "homed"). **No seed epochs, never TOTP codes.** Hint footer always printed (join/show-totp/create one-liners). Zero-subnet case renders explanatory text ("standalone node — local messaging works without one") + create/join hints. |
| 6 | `status --nodes`: per-node rows — node label, online/offline, `[online endpoints/total]`. Liveness = **hybrid**: gossip-recency for the table, probe only stale rows. |
| 7 | **Node labels**: default = OS hostname, re-checked at daemon startup (hostname change → label update), advertised via existing registry gossip (additive serde-default field). Render `HFENDULEAM (bcead52b…)`. Labels are **addressable** in `@node` qualifiers (label or key-prefix); non-unique label → refuse-and-qualify with candidate key prefixes, never guess. |
| 8 | **Responder always-on** in every member daemon (pre-trust pairing ALPN), per ADR-0005's one-sided-UX intent; the ADR red-team #11 subnet-global rate limiter is the standing-listener guard. No arming step. |
| 9 | **Elevation gates** (existing `EXIT_NOT_ELEVATED=3` convention): `subnet create` (seed reveal) and `subnet join` (trust-boundary mutation — an unprivileged process must not enroll the machine into an attacker's subnet) and `show-totp` (already gated). `status` ungated (read-only, no secrets). → ADR-0005 amendment, CONTEXT.md updated. |
| 10 | Guided join flow (joiner side, interactive when args missing): prompt name → prompt code → LAN+relay rendezvous search → ceremony → "Joined \"home\" — 2 nodes: …". Wrong code → clean re-prompt (fresh window), bounded attempts, rate-limit messages surfaced honestly. No-seed-holder-found → actionable hints (member online? same name? clock sync?). First-join home adoption (REQ-INST-15) fires on join as on create. |
| 11 | `subnet create` output: created-confirmation, current code, `otpauth://` URI, **terminal QR render** (unicode blocks; small `qrcode`-class crate), "run `spt subnet join <name>` on the other machine" hint. |
| 12 | **`spt how-to <topic>`**: in-binary, task-oriented agent instructions (anti-drift: single source = binary; docs-site never duplicates the content). Visible command, **Agent commands** group. Topics v1: `ready` (background-task guidance, `--once` fallback loop, reply mechanics), `send` (send/ring, reply-to, SENT vs QUEUED); bare `how-to` lists topics. Quickstart prompts point agents at it. |
| 13 | Quickstart rewrite: install (human) → **optional collapsed `<details>` pairing section** (human, elevated, create/join/status + cross-machine payoff line "same prompts now work across machines"; no captured two-machine example) → agent **prompt blocks** (copy-paste into agent sessions; agents run `spt how-to ready` / `how-to send` and follow) → offline-delivery demo → "what just happened". |
| 14 | Names: alice→**lea** (sender), bob→**sergey** (receiver). Scope: docs-site (Pages) + releases-repo README + `quickstart_e2e.rs`. Internal docs (CONTEXT.md, ADRs, plans — `ling`/`doyle`) unchanged. |
| 15 | README (releases-repo): quickstart bullet corrected to `ready`-based flow + pairing mention; status table "send / ring / ready / poll" loses `poll`. |
| 16 | New requirement family minted **before work starts** (TRACEABILITY rule 3): `REQ-SUBNET-*` (namespace surface · guided join e2e · node labels/addressable · elevation-gated membership), REQ-MSG-2 amended (poll merge), REQ-DOCS-x for `how-to`, REQ-PAIR-4/6 notes updated. Exact ids assigned at execution. |

## Deliverables

**D1 — CLI surface** (no new machinery):
`subnet` namespace skeleton + `create`/`show-totp` ports off `pair show-totp`
(+ QR render), `pair` namespace deleted, `poll` removed / `ready --once` added,
`how-to` command + topic texts, help-group placement, unit tests
(`help_groups_cover_every_command` keeps passing), `cli/reference.md` regen.

**D2 — node labels & status views**:
label field in registry gossip (additive serde-default; old rows parse clean),
hostname detection at daemon startup, label/key-prefix `@node` resolution with
refuse-on-ambiguity, `subnet status [NAME] [--nodes]` renders (hybrid
liveness), zero-subnet text.

**D3 — join machinery** (the new feature):
daemon hosts the pre-trust pairing responder ALPN always-on (rate limiter +
one-ceremony-per-subnet riding it), CLI initiator (guided + non-interactive),
elevation gates on `create`/`join`, seed transfer + trust pinning + home
adoption on join, failure-mode UX. ADR-0005 amendment recorded.

**D4 — docs**:
quickstart rewrite per decision 13/14, networking overview rewritten around
`subnet` verbs (kills "walkthrough coming" stub), messaging overview `poll`
sweep, README updates, llms.txt links, full lea/sergey sweep of docs-site,
reference regen (drift gate).

**D5 — CI & acceptance**:
`quickstart_e2e.rs` rewritten to the new guide step-for-step (`ready`,
`ready --once`, `how-to` output asserted), twohost ladder gains a
**product-surface join rung** (CLI initiator against daemon-hosted responder —
replaces the test-harness-driven ceremony as the pairing evidence),
`traceable-reqs check` green, `xtask check` green.

Order: D1 → D2 ∥ D3 → D4 → D5 (docs last so captured outputs are real).

## Acceptance

1. Fresh two-machine setup completes via `spt subnet create` + `spt subnet join`
   only — no test harness, elevation prompts honored, `subnet status --nodes`
   shows both nodes labeled and online on both machines.
2. Quickstart runs as written: a human pastes the prompt blocks into two agent
   sessions; messages flow; the collapsed pairing section upgrades the same
   flow to two machines.
3. `spt poll` is gone (`unknown subcommand`); `spt ready --once` drains; e2e
   suite green.
4. No doc page references `spt pair`, `spt poll`, alice, or bob.
