        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "show-totp", "home"]),
            Some(SubnetCmd::ShowTotp { name: Some(n) }) if n == "home"
        ));
    }

        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "show-totp", "home"]),
            Some(SubnetCmd::ShowTotp { name: Some(n) }) if n == "home"
        ));
        // [unit->REQ-SUBNET-2] the join shapes: fully interactive (bare),
        // name-only (prompts the code), and fully non-interactive.
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "join"]),
            Some(SubnetCmd::Join { name: None, code: None })
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "join", "home"]),
            Some(SubnetCmd::Join { name: Some(n), code: None }) if n == "home"
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "join", "home", "--code", "123456"]),
            Some(SubnetCmd::Join { name: Some(_), code: Some(c) }) if c == "123456"
        ));
    }

    // [unit->REQ-SUBNET-4] join is elevation-gated with the same fail-safe
    // posture as the seed-reveal paths: only CONFIRMED elevation proceeds;
    // NotElevated and the undeterminable Unknown both refuse (before any
    // prompt or network work — the command checks this gate first).
    #[test]
    fn join_gate_refuses_without_confirmed_elevation() {
        assert!(join_elevation_refusal(Elevation::Elevated).is_none());
        for level in [Elevation::NotElevated, Elevation::Unknown] {
            let msg = join_elevation_refusal(level).expect("refused");
            assert!(msg.contains("ELEVATION_REQUIRED"));
            assert!(msg.contains("administrator"), "actionable fix named: {msg}");
        }
    }

+        // [unit->REQ-SUBNET-2] the join shapes: fully interactive (bare),

+        // name-only (prompts the code), and fully non-interactive.

+            subnet_cmd(&["spt", "subnet", "join"]),

+            Some(SubnetCmd::Join { name: None, code: None })

+            subnet_cmd(&["spt", "subnet", "join", "home"]),

+            Some(SubnetCmd::Join { name: Some(n), code: None }) if n == "home"

+            Some(SubnetCmd::Join { name: Some(_), code: Some(c) }) if c == "123456"

+    // [unit->REQ-SUBNET-4] join is elevation-gated with the same fail-safe

+    // posture as the seed-reveal paths: only CONFIRMED elevation proceeds;

+    // NotElevated and the undeterminable Unknown both refuse (before any

+    // prompt or network work — the command checks this gate first).

+    fn join_gate_refuses_without_confirmed_elevation() {

+        assert!(join_elevation_refusal(Elevation::Elevated).is_none());

+        for level in [Elevation::NotElevated, Elevation::Unknown] {

+            let msg = join_elevation_refusal(level).expect("refused");

+            assert!(msg.contains("ELEVATION_REQUIRED"));

+            assert!(msg.contains("administrator"), "actionable fix named: {msg}");

// [unit->REQ-SUBNET-1] the `pair` namespace is deleted outright (no shim).


Edit

    // [unit->REQ-SUBNET-1] an unelevated create refuses without minting or
    // leaking (the seed-reveal posture; exit 3 like show-totp).
    #[test]
    fn create_not_elevated_refuses_without_saving() {

    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-4] an unelevated create refuses
    // without minting or leaking (the seed-reveal posture; exit 3 like
    // show-totp) — the create half of the membership-mutation gates.
    #[test]
    fn create_not_elevated_refuses_without_saving() {


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)

    // [unit->REQ-SUBNET-1] an unelevated create refuses without minting or
    // leaking (the seed-reveal posture; exit 3 like show-totp).
    #[test]
    fn create_not_elevated_refuses_without_saving() {

    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-4] an unelevated create refuses
    // without minting or leaking (the seed-reveal posture; exit 3 like
    // show-totp) — the create half of the membership-mutation gates.
    #[test]
    fn create_not_elevated_refuses_without_saving() {

-    // [unit->REQ-SUBNET-1] an unelevated create refuses without minting or

-    // leaking (the seed-reveal posture; exit 3 like show-totp).

+    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-4] an unelevated create refuses

+    // without minting or leaking (the seed-reveal posture; exit 3 like

+    // show-totp) — the create half of the membership-mutation gates.


/// Decide `subnet create`'s rendered outcome from a resolution + clock + elevation.
/// Same gate-last shape as [`decide_show_totp`] — a refused mint persists nothing and
/// leaks nothing — but the elevated output is the full joining material (M7 plan
/// decision 11): code, `otpauth://` URI, terminal QR, and the join hint.
// [impl->REQ-SUBNET-1]

/// Decide `subnet create`'s rendered outcome from a resolution + clock + elevation.
/// Same gate-last shape as [`decide_show_totp`] — a refused mint persists nothing and
/// leaks nothing — but the elevated output is the full joining material (M7 plan
/// decision 11): code, `otpauth://` URI, terminal QR, and the join hint. The gate is
/// the create half of the membership-mutation elevation requirement (a mint REVEALS
/// the subnet's joining secret).
// [impl->REQ-SUBNET-1] [impl->REQ-SUBNET-4]


/// Decide `subnet create`'s rendered outcome from a resolution + clock + elevation.
/// Same gate-last shape as [`decide_show_totp`] — a refused mint persists nothing and
/// leaks nothing — but the elevated output is the full joining material (M7 plan
/// decision 11): code, `otpauth://` URI, terminal QR, and the join hint.
// [impl->REQ-SUBNET-1]

/// Decide `subnet create`'s rendered outcome from a resolution + clock + elevation.
/// Same gate-last shape as [`decide_show_totp`] — a refused mint persists nothing and
/// leaks nothing — but the elevated output is the full joining material (M7 plan
/// decision 11): code, `otpauth://` URI, terminal QR, and the join hint. The gate is
/// the create half of the membership-mutation elevation requirement (a mint REVEALS
/// the subnet's joining secret).
// [impl->REQ-SUBNET-1] [impl->REQ-SUBNET-4]

-/// decision 11): code, `otpauth://` URI, terminal QR, and the join hint.

+/// decision 11): code, `otpauth://` URI, terminal QR, and the join hint. The gate is

+/// the create half of the membership-mutation elevation requirement (a mint REVEALS

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

Resolved::Subnet { name, seed, minted } => match elevation::gate_for(elevation) {


Activate REQ-SUBNET-2 + REQ-SUBNET-4:


title = "Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder"
required_stages = []  # M7 D3 (plan decisions 8, 10)

title = "Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder"
required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): pairhost responder/meet-rotation/joiner leg + pair-join IPC + guided CLI land; int (twohost product-surface join rung) added at D5. M7 D3 (plan decisions 8, 10)


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

title = "Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder"
required_stages = []  # M7 D3 (plan decisions 8, 10)

title = "Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder"
required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): pairhost responder/meet-rotation/joiner leg + pair-join IPC + guided CLI land; int (twohost product-surface join rung) added at D5. M7 D3 (plan decisions 8, 10)

-required_stages = []  # M7 D3 (plan decisions 8, 10) — the join ceremony's PRODUCT surface (the SPAKE2 wire is rig-proven but test-only today). Every member daemon hosts the pre-trust pairing ALPN responder ALWAYS-ON (ADR-0005 one-sided-UX intent; the red-team #11 subnet-global rate limiter + one-ceremony-per-subnet is the standing-listener guard). `spt subnet join [NAME] [--code <CODE>]`: prompts for missing pieces, LAN+relay rendezvous, clean wrong-code re-prompt (bounded, limiter surfaced honestly), actionable no-seed-holder hints, seed transfer + trust pinning + REQ-INST-15 home adoption on join. int = twohost ladder gains a product-surface join rung (CLI initiator vs daemon responder) replacing the test-harness-driven ceremony as pairing evidence.

+required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): pairhost responder/meet-rotation/joiner leg + pair-join IPC + guided CLI land; int (twohost product-surface join rung) added at D5. M7 D3 (plan decisions 8, 10) — the join ceremony's PRODUCT surface (the SPAKE2 wire is rig-proven but test-only today). Every member daemon hosts the pre-trust pairing ALPN responder ALWAYS-ON (ADR-0005 one-sided-UX intent; the red-team #11 subnet-global rate limiter + one-ceremony-per-subnet is the standing-listener guard). `spt subnet join [NAME] [--code <CODE>]`: prompts for missing pieces, LAN+relay rendezvous, clean wrong-code re-prompt (bounded, limiter surfaced honestly), actionable no-seed-holder hints, seed transfer + trust pinning + REQ-INST-15 home adoption on join. int = twohost ladder gains a product-surface join rung (CLI initiator vs daemon responder) replacing the test-harness-driven ceremony as pairing evidence.

title = "Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)"


title = "Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)"
required_stages = []  # M7 D3 (plan decision 9; ADR-0005 amendment lands with it).

title = "Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)"
required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): create + join gates live (REQ-PAIR-6 machinery, exit 3, gate-first ordering); ADR-0005 amendment recorded. M7 D3 (plan decision 9).


title = "Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)"
required_stages = []  # M7 D3 (plan decision 9; ADR-0005 amendment lands with it).

title = "Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)"
required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): create + join gates live (REQ-PAIR-6 machinery, exit 3, gate-first ordering); ADR-0005 amendment recorded. M7 D3 (plan decision 9).

-required_stages = []  # M7 D3 (plan decision 9; ADR-0005 amendment lands with it). `subnet create` and `subnet join` require OS elevation via the existing REQ-PAIR-6 gate machinery (exit 3 on refusal, no leak/no mutation): an unprivileged process must not mint+leak a subnet secret nor enroll the machine into an attacker's subnet. `subnet status` stays ungated (read-only, no secrets). CONTEXT.md §Pairing & trust updated 2026-06-05.

+required_stages = ["impl", "unit"]  # activated M7 D3 (2026-06-05): create + join gates live (REQ-PAIR-6 machinery, exit 3, gate-first ordering); ADR-0005 amendment recorded. M7 D3 (plan decision 9). `subnet create` and `subnet join` require OS elevation via the existing REQ-PAIR-6 gate machinery (exit 3 on refusal, no leak/no mutation): an unprivileged process must not mint+leak a subnet secret nor enroll the machine into an attacker's subnet. `subnet status` stays ungated (read-only, no secrets). CONTEXT.md §Pairing & trust updated 2026-06-05.

title = "spt how-to <topic>: in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it)"


Bash

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

Build + run spt unit tests


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


D3a: pairhost module + nethost multi-ALPN routing + meet rotation

crates/spt-daemon/src/pairhost.rs: respond() handler (tokio Mutex limiter try_lock shed, 60s timeout aligned w/ CEREMONY_TIMEOUT_SECS), meet rotation loop per member subnet (re-bound per TOTP step, soft-fail binds, skip on BindScope::Port), join_ceremony() joiner leg. NetHost binds [SPT_NET_ALPN, SPT_PAIR_ALPN], accept loop routes by conn.alpn().

D3b: pair-join broker IPC op + brain client method

msg.rs KIND_PAIR_JOIN/JOINED + PairJoinReq/Reply (ok, class, detail). broker.rs dispatch arm → NetHost::pair_join. brain.rs client pair_join().

D3c: spt subnet join CLI + elevation gates + REQ-SUBNET-2/4 activation

join [NAME] [--code] — elevation gate (exit 3), interactive prompts when tty, bounded wrong-code re-prompt, honest rate-limit + no-seed-holder hints, home adoption on success. Status hint footer gains join line. Activate REQ-SUBNET-2 [impl] (+unit), REQ-SUBNET-4 [impl,unit]; tag create/join gates.

D3d: ADR-0005 amendment + full checks + commit

Record always-on responder + elevation gates amendment in docs/adr/0005. cargo tests, xtask gen/check, traceable-reqs check, commit.


Updated task #11 status


Updated task #12 status


cargo build -p spt 2>&1 | tail -1 && SPT_HOME=$(mktemp -d) ./target/debug/spt.exe subnet join testnet --code 123456; echo "exit=$?"

Smoke-test join elevation gate


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 22.96s
ELEVATION_REQUIRED: joining a subnet enrolls this machine into its trust fabric — re-run elevated (run as administrator / root)
exit=3


grep -n "^## \|^# \|Amendment" docs/adr/0005-totp-seeded-spake2-node-pairing.md | head -20; tail -30 docs/adr/0005-totp-seeded-spake2-node-pairing.md

Survey ADR-0005 structure


1:# TOTP-seeded SPAKE2 node pairing
3:## Status
7:## Context
13:## Decision
30:## Consequences
39:## Stage A red-team amendments (2026-05-31)
47:## Multi-subnet amendment (2026-06-01 — see ADR-0006)

Trust model: local per-node trust store of authorized peer pubkeys; TOFU after pairing (accept on first post-ceremony connect, warn on key change). Cross-subnet (two different users) is out of scope for v1.

## Consequences

- A seed-holder node must be online and reachable by the untrusted new node at pairing time; the relay/discovery layer must permit pre-trust contact on a dedicated pairing ALPN.
- Operational details to implement: ±1 TOTP window tolerance, handshake-attempt rate limiting, QR re-provisioning for recovery from any still-trusted node.
- Losing all nodes *and* the authenticator seed makes the subnet unrecoverable — must be documented.
- Revocation of a paired node is a separate trust-store delete; TOTP gates joining only, not continued membership.
- Adds a SPAKE2 + TOTP dependency to the core crypto surface (both have mature Rust crates per the research brief).
- Because every paired node holds the seed, the current code is fetchable from any trusted node (`spt pair --code` or via an agent) — no phone needed if a trusted node is handy. The user is prompted to **name the subnet** on first pairing; the name is shown on every subsequent pairing for confidence as the fleet grows.

## Stage A red-team amendments (2026-05-31)

Codex (`docs/reviews/STAGE-A-codex-redteam.md`) found three issues; resolve before M4 builds pairing:

- **FATAL #10 — trust-store delete is NOT revocation.** The "revocation = trust-store delete; TOTP gates joining only" consequence above is false in the threat model: the seed is replicated to every trusted node, so a compromised node/seed can re-pair as a *new* node after deletion. **Required:** a seed **epoch + rotation** protocol — removing a compromised node rotates the subnet seed so the old node/old seed cannot rejoin. Pairing transcript must bind the seed epoch.
- **SERIOUS #11 — SPAKE2's one-online-guess bound needs subnet-global rate limiting.** A public pre-trust relay + multiple seed-holding nodes enables *distributed* guessing, and a ±1 TOTP acceptance window triples the valid-password space. **Required:** one active pairing ceremony per subnet, shared attempt counter, exponential backoff; justify or drop the ±1 window.
- **SERIOUS #12 — "PAKE binds pubkeys" only if the transcript does.** Must explicitly bind roles, both node pubkeys, subnet ID, TOTP time-step, and confirmation MACs, or unknown-key-share / reflection / wrong-subnet pairing remain. **Required:** full pairing-transcript spec + negative tests (MITM, replay, reflection, wrong subnet, stale time-step).

## Multi-subnet amendment (2026-06-01 — see ADR-0006)

ADR-0006 makes a node a member of *multiple* subnets. The pairing ceremony is refined (not reversed) accordingly:

- **Discovery takes two inputs: TOTP code + subnet-name.** The code is also the relay rendezvous selector, but a 6-digit code is a weak global selector, so the subnet-name namespaces the rendezvous. This is the **unified rule for all cases** — there is no node-name mode.
- **"Create-new subnet" names the subnet at creation.** One machine generates the new seed *and* runs the R-PAIR-4 naming prompt up front (becoming the sole seed-holder); the joiner then uses that subnet-name + the new seed's code. Naming simply moves to link-start.
- **Rendezvous-token hashing.** The relay routes the pre-trust ceremony by `H(subnet-name ‖ TOTP-epoch)`, not the plaintext label — the user/agent enters the raw name + code; the hash is internal. (Payload is already E2E-encrypted; this protects the relay-visible routing token from leaking the human subnet label.) Composes with the #12 transcript binding (the subnet ID / seed epoch already in the transcript).
- **Per-subnet code fetch is gated behind OS elevation** (Windows UAC / Linux root-or-equivalent), or an elevated agent endpoint; else the user falls back to their authenticator app. Replaces the old unqualified "fetchable from any trusted node" with an elevation-gated, *per-subnet* fetch (a node now holds several subnets' seeds).
- **Code fetch is per-subnet** (a node holds *N* seeds): `spt pair show-totp` prompts which subnet (or `--subnet <name>` / `--create-new` to bypass), offered optionally as a QR / `otpauth://` URI.
- **Forward (cross-user, ADR-0006 seam):** the seed key becomes per-(subnet, user); the prompt becomes 2-stage. The #10 seed epoch+rotation and #11 subnet-global rate-limit requirements apply **per subnet**.


cat >> docs/adr/0005-totp-seeded-spake2-node-pairing.md << 'EOF'

## Product-surface amendment (2026-06-05 — M7 D3)

<!-- [doc->REQ-SUBNET-2] [doc->REQ-SUBNET-4] -->

M7 gives the ceremony its product surface (`M7-PLAN.md` decisions 8–10); two
postures are ratified as part of this ADR:

- **The responder is always-on in every member daemon.** The broker's
  node-identity endpoint speaks the pre-trust pairing ALPN beside the trusted
  surface (one endpoint, ALPN-separated — the transcript binds/pins the REAL
  node pubkey, and a relay routes one endpoint per key), and a per-subnet
  rendezvous **meet** listener rotates with the TOTP step. There is no arming
  step: this completes the one-sided UX this ADR intended ("a seed-holder
  node must be online and reachable"). The standing-listener exposure is
  guarded by the #11 subnet-global rate limiter, a one-ceremony-at-a-time
  shed at the accept door, and a ceremony timeout aligned with the limiter's
  stale-slot reclaim — an attacker reaching the listener still gets one
  online guess per admitted ceremony, serialized and backed off.
- **Membership mutations are elevation-gated** (the REQ-PAIR-6 machinery,
  exit 3, gate-before-everything): `subnet create` reveals a fresh subnet's
  joining secret, and `subnet join` enrolls the whole machine into a subnet's
  trust fabric — an unprivileged process must do neither (an attacker-run
  joiner would enroll the box into the attacker's subnet). `subnet status`
  stays ungated: read-only, membership shape only, never secrets.

Surface rename, same semantics: `spt pair show-totp [--subnet|--create-new]`
became `spt subnet show-totp [NAME]` / `spt subnet create <NAME>` /
`spt subnet join [NAME] [--code]` (M7 decision 3 — verbs, not flags; the
"user is prompted to name the subnet" consequence above is delivered by the
guided join/create prompts).
EOF
echo done

Append M7 D3 amendment to ADR-0005


done


cargo test -p spt-daemon 2>&1 | grep -E "test result" | awk '{s+=$4; f+=$6} END {print "passed:"s" failed:"f}'; cargo test -p spt 2>&1 | grep -E "test result" | awk '{s+=$4; f+=$6} END {print "passed:"s" failed:"f}'; cargo test -p spt-net 2>&1 | grep -E "test result" | awk '{s+=$4; f+=$6} END {print "passed:"s" failed:"f}'

Run all three suites


passed:225 failed:0
passed:97 failed:0
passed:99 failed:0


cargo run -p xtask -- gen 2>&1 | tail -1 && cargo run -p xtask -- check 2>&1 | tail -1 && traceable-reqs check >/dev/null 2>&1; echo "reqs=$?"

Regen reference + gates


xtask gen: wrote C:\Users\decid\Documents\projects\spt-core\docs-site/src/cli/reference.md
xtask check: OK
reqs=0


git add -A && git commit -m "$(cat <<'EOF'
feat(daemon,cli): M7 D3 — always-on pairing responder + guided spt subnet join

The join-machinery deliverable (M7-PLAN.md D3, REQ-SUBNET-2 + REQ-SUBNET-4)
— the SPAKE2/TOTP ceremony's product surface; the wire itself is unchanged:

- pairhost (new): the broker's node-identity endpoint now speaks the
  pre-trust pairing ALPN beside the trusted surface (one endpoint,
  ALPN-routed at accept — the transcript binds/pins the REAL node pubkey,
  and a relay routes one endpoint per key). Responder is ALWAYS-ON in every
  member daemon (decision 8): no arming step; guarded by the red-team #11
  subnet-global rate limiter + a one-ceremony-at-a-time shed at the accept
  door + a ceremony timeout aligned with the limiter's stale-slot reclaim.
  Per-subnet rendezvous meet listeners rotate with the TOTP step (soft-fail
  binds; Port-scoped rigs map to All — the fixed port belongs to the main
  endpoint); store re-read per window so a fresh subnet meets within one
  step. Joiner leg (join_ceremony) dials via LAN+relay rendezvous, runs the
  unchanged initiator, persists trust + the transferred seed; failures
  classify (JoinFail) for the guided UX.
- pair-join broker IPC (decision 10): KIND_PAIR_JOIN/JOINED +
  PairJoinReq/Reply; deliberately NOT journaled (the code must never land
  in a durable journal; re-join is store-idempotent). Brain::pair_join
  client method.
- spt subnet join [NAME] [--code] (decisions 9, 10): elevation gate FIRST
  (REQ-SUBNET-4 — enrollment is a trust-boundary mutation; exit 3, nothing
  typed/dialed/saved on refusal), interactive prompts when stdin is a tty,
  bounded wrong-code re-prompt (codes rotate every 30s), rate limits
  surfaced honestly, actionable no-seed-holder hints (member online? exact
  name? clocks ±30s?), REQ-INST-15 home adoption on join as on create.
  Status hint footer gains the join one-liner (decision 5 trio complete).
- ADR-0005 product-surface amendment recorded (always-on responder posture,
  elevation-gated membership mutations, surface rename map).
- REQ-SUBNET-2 + REQ-SUBNET-4 activated to [impl, unit] (rule 5); the int
  stage (twohost product-surface join rung) lands at D5.
- All suites green (spt 97, spt-daemon 225, spt-net 99); xtask check,
  traceable-reqs check, reference regen.

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

Commit M7 D3


warning: in the working copy of 'crates/spt-daemon/src/brain.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/broker.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/msg.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/nethost.rs', 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-site/src/cli/reference.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0005-totp-seeded-spake2-node-pairing.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/pairhost.rs', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 3df6e2b] feat(daemon,cli): M7 D3 — always-on pairing responder + guided spt subnet join
 10 files changed, 753 insertions(+), 31 deletions(-)
 create mode 100644 crates/spt-daemon/src/pairhost.rs
3df6e2b feat(daemon,cli): M7 D3 — always-on pairing responder + guided spt subnet join
3d909bb feat(net,daemon,cli): M7 D2 — node labels, addressable @node qualifiers, status --nodes
0f85f1d feat(cli): M7 D1 — spt subnet namespace, poll->ready --once merge, spt how-to


Write

M7 D3 (join machinery) COMPLETE — committed 3df6e2b on dev-freeform. Session has now shipped D1 (0f85f1d) → D2 (3d909bb) → D3 (3df6e2b).

D3 delta:
- New spt-daemon/src/pairhost.rs: always-on responder (broker endpoint speaks SPT_PAIR_ALPN beside SPT_NET_ALPN, ALPN-routed at accept; one-ceremony shed via tokio Mutex try_lock + 60s timeout aligned with limiter stale-slot reclaim), per-subnet meet rotation re-bound per TOTP step (soft-fail binds, Port scope→All), join_ceremony joiner leg with JoinFail classification.
- pair-join IPC (NOT journaled — code never persists), Brain::pair_join.
- `spt subnet join [NAME] [--code]`: elevation gate first (exit 3), tty prompts, bounded wrong-code re-prompt, honest rate-limit surfacing, no-seed-holder hints, home adoption on join. Hint footer trio complete.
- ADR-0005 product-surface amendment recorded. REQ-SUBNET-2/4 active [impl, unit].
- All gates green (spt 97 / spt-daemon 225 / spt-net 99, xtask check, traceable-reqs).

Remaining M7: **D4 (docs)** — quickstart rewrite (decisions 13/14: human install → collapsed pairing details → agent prompt blocks pointing at `spt how-to` → offline demo; alice/bob → lea/sergey across docs-site + releases-repo README + quickstart_e2e), networking overview rewritten around subnet verbs, messaging overview poll sweep, llms.txt links, reference regen. **D5 (CI & acceptance)** — quickstart_e2e rewrite step-for-step (ready/--once/how-to asserts → int tags REQ-DOCS-6), twohost ladder product-surface join rung (CLI initiator vs daemon responder → int tag REQ-SUBNET-2), full green.

NOTE for D5 twohost: role B's daemon-hosted responder now exists — the rung should drive `spt subnet join` (or pair_join IPC) on role A against role B's daemon instead of the test-harness run_initiator/run_responder pair; rig uses BindScope::Port + relay disabled, so joiner must dial explicit addr (meet listener skipped under Port scope — consider meet_at explicit-address seam or env-provided responder addr as the rig already does).


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.claude\doyle-commune.md (file state is current in your context — no need to Read it back)

M7 D3 (join machinery) COMPLETE — committed 3df6e2b on dev-freeform. Session has now shipped D1 (0f85f1d) → D2 (3d909bb) → D3 (3df6e2b).

D3 delta:
- New spt-daemon/src/pairhost.rs: always-on responder (broker endpoint speaks SPT_PAIR_ALPN beside SPT_NET_ALPN, ALPN-routed at accept; one-ceremony shed via tokio Mutex try_lock + 60s timeout aligned with limiter stale-slot reclaim), per-subnet meet rotation re-bound per TOTP step (soft-fail binds, Port scope→All), join_ceremony joiner leg with JoinFail classification.
- pair-join IPC (NOT journaled — code never persists), Brain::pair_join.
- `spt subnet join [NAME] [--code]`: elevation gate first (exit 3), tty prompts, bounded wrong-code re-prompt, honest rate-limit surfacing, no-seed-holder hints, home adoption on join. Hint footer trio complete.
- ADR-0005 product-surface amendment recorded. REQ-SUBNET-2/4 active [impl, unit].
- All gates green (spt 97 / spt-daemon 225 / spt-net 99, xtask check, traceable-reqs).

Remaining M7: **D4 (docs)** — quickstart rewrite (decisions 13/14: human install → collapsed pairing details → agent prompt blocks pointing at `spt how-to` → offline demo; alice/bob → lea/sergey across docs-site + releases-repo README + quickstart_e2e), networking overview rewritten around subnet verbs, messaging overview poll sweep, llms.txt links, reference regen. **D5 (CI & acceptance)** — quickstart_e2e rewrite step-for-step (ready/--once/how-to asserts → int tags REQ-DOCS-6), twohost ladder product-surface join rung (CLI initiator vs daemon responder → int tag REQ-SUBNET-2), full green.

NOTE for D5 twohost: role B's daemon-hosted responder now exists — the rung should drive `spt subnet join` (or pair_join IPC) on role A against role B's daemon instead of the test-harness run_initiator/run_responder pair; rig uses BindScope::Port + relay disabled, so joiner must dial explicit addr (meet listener skipped under Port scope — consider meet_at explicit-address seam or env-provided responder addr as the rig already does).


Bash

ls docs-site/src docs-site/src/quickstart docs-site/src/networking docs-site/src/messaging; echo ---; grep -rn "alice\|bob" docs-site/src releases-repo --include="*.md" -l 2>/dev/null; echo ---; grep -rn "spt pair\|spt poll" docs-site/src releases-repo --include="*.md" -l 2>/dev/null

Survey docs-site for D4 targets


docs-site/src:
SUMMARY.md
cli
concepts
harness-contract
index.md
instances
lifecycle
messaging
networking
quickstart
reference
self-update
shells
terminal

docs-site/src/messaging:
overview.md

docs-site/src/networking:
overview.md

docs-site/src/quickstart:
adapter.md
messaging.md
---
docs-site/src/concepts/overview.md
docs-site/src/instances/overview.md
docs-site/src/messaging/overview.md
docs-site/src/networking/overview.md
docs-site/src/quickstart/messaging.md
---
docs-site/src/networking/overview.md
docs-site/src/quickstart/messaging.md


1	# Quickstart: two agents exchange a message
2
3	<!-- [doc->REQ-DOCS-2] the human killer quickstart: install -> two agents message, real captured outputs, <10 min; CI-run step for step by quickstart_e2e.rs -->
4	End to end in under 10 minutes: install spt-core, bring two agents online, and
5	pass real messages between them — including one delivered while the receiver
6	was offline.
7
8	> This is the developer path. Building an adapter or integrating a harness?
9	> Go to the [adapter quickstart](adapter.md) instead.
10
11	Everything below uses real values and runs as written. You need two terminals.
12
13	## 1. Install (one line)
14
15	```sh
16	# Linux
17	curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
18	```
19
20	```powershell
21	# Windows (PowerShell)
22	irm https://sabermage.github.io/spt-releases/install.ps1 | iex
23	```
24
25	Verify (on Windows, open a **new** terminal first — or use the absolute path
26	the installer printed):
27
28	```console
29	$ spt --version
30	spt 0.1.0
31	```
32
33	## 2. Bring an agent online
34
35	In **terminal B**, become reachable as `bob`:
36
37	```console
38	$ spt poll bob
39	READY:bob
40	```
41
42	`poll` registers a *perch* for `bob` (his identity + address on this machine),
43	drains any backlog, and blocks listening. Leave it running.
44
45	## 3. Send him a message
46
47	In **terminal A**:
48
49	```console
50	$ echo "hello bob - alice here" | spt send bob --from alice
51	SENT:bob
52	```
53
54	(Windows PowerShell: `"hello bob - alice here" | spt send bob --from alice`.)
55
56	Terminal B prints it immediately:
57
58	```text
59	__REPLY_TO__:alice
60	hello bob - alice here
61	```
62
63	`SENT` means live delivery — bob was listening, so the message went straight
64	to his terminal over a local connection. The `__REPLY_TO__:alice` line is the
65	routing header: whoever receives this knows where a reply goes.
66
67	## 4. Deliver to someone who's offline
68
69	Stop bob (Ctrl-C in terminal B), then send again from terminal A:
70
71	```console
72	$ echo "ping while you were away" | spt send bob --from alice
73	QUEUED:bob
74	```
75
76	`QUEUED` means bob has a perch but isn't listening — the message went to his
77	durable spool instead of being dropped. Bring him back in terminal B:
78
79	```console
80	$ spt poll bob
81	READY:bob
82	__REPLY_TO__:alice
83	ping while you were away
84	```
85
86	The backlog drains the moment he's back. Nothing is lost between sessions.
87
88	## 5. What just happened
89
90	- **Perch** — registering as `bob` created a perch: a durable identity with an
91	  address and a spool, under spt-core's per-machine home. `spt list` shows
92	  every perch on the node, live or not.
93	- **Live-first, spool-fallback** — `send` tries a direct connection to the
94	  registered address first (`SENT`); if the perch exists but no listener is
95	  up, the message lands in the spool (`QUEUED`) and is drained by the next
96	  `poll`/`ready`.
97	- **Reply routing** — the `__REPLY_TO__` header travels with every message;
98	  `spt send --reply-to alice` answers the sender without knowing anything
99	  else about them.
100	- **No daemon ceremony** — you never started a server. Anything that needs
101	  the per-machine daemon auto-starts it on demand.
102
103	## Next
104
105	- **How-to:** block on an answer with `spt ring bob` — send + wait for the
106	  reply in one call (a synchronous ask between agents).
107	- **Concept:** the [mental model](../concepts/overview.md) — perches,
108	  endpoints, the daemon, and subnets.
109	- **Reference:** [`spt send` / `poll` / `ring` / `list`](../cli/reference.md)
110	  — every flag, generated from the binary itself.
111	- **Going cross-machine:** [Networking & pairing](../networking/overview.md)
112	  — pair a second machine with a TOTP code and the same `send` reaches
113	  `bob` on the other box.
114

# Quickstart: two agents exchange a message

<!-- [doc->REQ-DOCS-2] the human killer quickstart: install -> two agents message, real captured outputs, <10 min; CI-run step for step by quickstart_e2e.rs -->
End to end in under 10 minutes: install spt-core, bring two agents online, and
pass real messages between them — including one delivered while the receiver
was offline.

> This is the developer path. Building an adapter or integrating a harness?
> Go to the [adapter quickstart](adapter.md) instead.

Everything below uses real values and runs as written. You need two terminals.

## 1. Install (one line)

```sh
# Linux
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

```powershell
# Windows (PowerShell)
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
```

Verify (on Windows, open a **new** terminal first — or use the absolute path
the installer printed):

```console
$ spt --version
spt 0.1.0
```

## 2. Bring an agent online

In **terminal B**, become reachable as `bob`:

```console
$ spt poll bob
READY:bob
```

`poll` registers a *perch* for `bob` (his identity + address on this machine),
drains any backlog, and blocks listening. Leave it running.

## 3. Send him a message

In **terminal A**:

```console
$ echo "hello bob - alice here" | spt send bob --from alice
SENT:bob
```

(Windows PowerShell: `"hello bob - alice here" | spt send bob --from alice`.)

Terminal B prints it immediately:

```text
__REPLY_TO__:alice
hello bob - alice here
```

`SENT` means live delivery — bob was listening, so the message went straight
to his terminal over a local connection. The `__REPLY_TO__:alice` line is the
routing header: whoever receives this knows where a reply goes.

## 4. Deliver to someone who's offline

Stop bob (Ctrl-C in terminal B), then send again from terminal A:

```console
$ echo "ping while you were away" | spt send bob --from alice
QUEUED:bob
```

`QUEUED` means bob has a perch but isn't listening — the message went to his
durable spool instead of being dropped. Bring him back in terminal B:

```console
$ spt poll bob
READY:bob
__REPLY_TO__:alice
ping while you were away
```

The backlog drains the moment he's back. Nothing is lost between sessions.

## 5. What just happened

- **Perch** — registering as `bob` created a perch: a durable identity with an
  address and a spool, under spt-core's per-machine home. `spt list` shows
  every perch on the node, live or not.
- **Live-first, spool-fallback** — `send` tries a direct connection to the
  registered address first (`SENT`); if the perch exists but no listener is
  up, the message lands in the spool (`QUEUED`) and is drained by the next
  `poll`/`ready`.
- **Reply routing** — the `__REPLY_TO__` header travels with every message;
  `spt send --reply-to alice` answers the sender without knowing anything
  else about them.
- **No daemon ceremony** — you never started a server. Anything that needs
  the per-machine daemon auto-starts it on demand.

## Next

- **How-to:** block on an answer with `spt ring bob` — send + wait for the
  reply in one call (a synchronous ask between agents).
- **Concept:** the [mental model](../concepts/overview.md) — perches,
  endpoints, the daemon, and subnets.
- **Reference:** [`spt send` / `poll` / `ring` / `list`](../cli/reference.md)
  — every flag, generated from the binary itself.
- **Going cross-machine:** [Networking & pairing](../networking/overview.md)
  — pair a second machine with a TOTP code and the same `send` reaches
  `bob` on the other box.


1	# Networking & pairing
2
3	Zero-config, no-central-server connectivity between your machines. Pair two
4	nodes once with a six-digit code; from then on, the same `spt send bob` works
5	whether bob is local or three networks away.
6
7	## The model
8
9	- **Node identity** — each machine holds an Ed25519 keypair; the public key
10	  *is* its network identity. Connections are mutually authenticated QUIC,
11	  end-to-end encrypted, peer-to-peer with NAT hole-punching and public-relay
12	  fallback (you can self-host the relay, or disable it for LAN/air-gapped
13	  use — the default relays carry only encrypted traffic they cannot read).
14	- **Subnets** — machines pair into named groups. A subnet shares: the
15	  endpoint registry (who exists, where, what state), context sync for its
16	  endpoints, notifications, and staged self-updates. Nothing is shared with
17	  unpaired nodes, ever.
18	- **Pairing** — a one-time ceremony seeded by a TOTP code: run
19	  `spt pair show-totp` on a member node (it also prints an `otpauth://` URI
20	  — put the seed in your authenticator app), type the subnet name + current
21	  six digits on the joiner. The code bootstraps a PAKE key exchange — the
22	  code is never the key, and a wrong guess learns nothing. Both sides pin
23	  each other's node keys on success (trust-on-first-use; key changes warn
24	  and never auto-apply).
25	- **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be
26	  hidden from a subnet (neither advertised nor routable) and its mind syncs
27	  only to subnets on its membership list. Both default conservative;
28	  unconfigured means *not shared*.
29	- **Resource registry** — endpoints may advertise a free-text service blurb
30	  (`spt resources set/list`) — an agent yellow-pages over visible rows only.
31
32	## What rides it
33
34	Cross-machine `send`/`ring`, registry replication, two-tier mind sync,
35	remote attach, remote suspend/wake, file transfer, notification replication,
36	and peer-propagated self-update — all over the same paired substrate.
37
38	## Commands
39
40	`spt pair` · `spt resources` · the qualified addressing forms
41	(`[subnet:]id[@node]`) — [CLI reference](../cli/reference.md).
42
43	*Pairing walkthrough tutorial coming with the docs' next tier.*
44

# Networking & pairing

Zero-config, no-central-server connectivity between your machines. Pair two
nodes once with a six-digit code; from then on, the same `spt send bob` works
whether bob is local or three networks away.

## The model

- **Node identity** — each machine holds an Ed25519 keypair; the public key
  *is* its network identity. Connections are mutually authenticated QUIC,
  end-to-end encrypted, peer-to-peer with NAT hole-punching and public-relay
  fallback (you can self-host the relay, or disable it for LAN/air-gapped
  use — the default relays carry only encrypted traffic they cannot read).
- **Subnets** — machines pair into named groups. A subnet shares: the
  endpoint registry (who exists, where, what state), context sync for its
  endpoints, notifications, and staged self-updates. Nothing is shared with
  unpaired nodes, ever.
- **Pairing** — a one-time ceremony seeded by a TOTP code: run
  `spt pair show-totp` on a member node (it also prints an `otpauth://` URI
  — put the seed in your authenticator app), type the subnet name + current
  six digits on the joiner. The code bootstraps a PAKE key exchange — the
  code is never the key, and a wrong guess learns nothing. Both sides pin
  each other's node keys on success (trust-on-first-use; key changes warn
  and never auto-apply).
- **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be
  hidden from a subnet (neither advertised nor routable) and its mind syncs
  only to subnets on its membership list. Both default conservative;
  unconfigured means *not shared*.
- **Resource registry** — endpoints may advertise a free-text service blurb
  (`spt resources set/list`) — an agent yellow-pages over visible rows only.

## What rides it

Cross-machine `send`/`ring`, registry replication, two-tier mind sync,
remote attach, remote suspend/wake, file transfer, notification replication,
and peer-propagated self-update — all over the same paired substrate.

## Commands

`spt pair` · `spt resources` · the qualified addressing forms
(`[subnet:]id[@node]`) — [CLI reference](../cli/reference.md).

*Pairing walkthrough tutorial coming with the docs' next tier.*


1	# Messaging
2
3	The substrate everything else rides on: durable, addressed, reply-routable
4	messages between endpoints — live when the target listens, spooled when it
5	doesn't, across machines once nodes are paired.
6
7	You've probably already run the
8	[quickstart](../quickstart/messaging.md); this page is the model.
9
10	## Semantics
11
12	- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
13	  listening target (`SENT`); if the perch exists but nothing is listening,
14	  the message lands in the target's durable spool (`QUEUED`) and drains on
15	  its next `poll`/`ready`. A target with *no* perch is an error (`NO_PERCH`)
16	  — identity is never invented on someone else's behalf.
17	- **Reply routing.** Every message carries a `__REPLY_TO__` header;
18	  `spt send --reply-to <sender>` answers without knowing anything else.
19	- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
20	  a timeout) — the synchronous question between agents.
21	- **Deferred delivery.** `--deferred` spools without waking a live listener:
22	  for context that should reach the agent at its next natural boundary
23	  rather than interrupting now. Deferred messages are also held for resting
24	  (dormant/suspended) instances and released exactly once on wake.
25	- **Typed payloads.** Message bodies carry typed operations and file blobs,
26	  not just text — file transfers are addressable and progress-queryable
27	  mid-flight.
28
29	## Addressing
30
31	Bare ids (`bob`) resolve locally first, then across the subnet; when the
32	same id is live on several nodes, resolution **refuses and asks you to
33	qualify** (`bob@desktop`) rather than guessing. The full form is
34	`[subnet:]id[@node]`.
35
36	## Commands
37
38	`send` · `ring` · `ready` · `poll` · `list` · `stop` · `whoami` — every flag
39	in the [CLI reference](../cli/reference.md).
40

# Messaging

The substrate everything else rides on: durable, addressed, reply-routable
messages between endpoints — live when the target listens, spooled when it
doesn't, across machines once nodes are paired.

You've probably already run the
[quickstart](../quickstart/messaging.md); this page is the model.

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `poll`/`ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries a `__REPLY_TO__` header;
  `spt send --reply-to <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents.
- **Deferred delivery.** `--deferred` spools without waking a live listener:
  for context that should reach the agent at its next natural boundary
  rather than interrupting now. Deferred messages are also held for resting
  (dormant/suspended) instances and released exactly once on wake.
- **Typed payloads.** Message bodies carry typed operations and file blobs,
  not just text — file transfers are addressable and progress-queryable
  mid-flight.

## Addressing

Bare ids (`bob`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`bob@desktop`) rather than guessing. The full form is
`[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` · `poll` · `list` · `stop` · `whoami` — every flag
in the [CLI reference](../cli/reference.md).


grep -n "alice\|bob" docs-site/src/concepts/overview.md docs-site/src/instances/overview.md docs-site/src/index.md docs-site/llms.txt 2>/dev/null; echo ---README---; grep -n "poll\|pair\|alice\|bob" releases-repo/README.md 2>/dev/null | head; ls releases-repo

Find remaining rename/poll targets


docs-site/src/concepts/overview.md:23: │  │ sockets│    │ registry ·│ │        │   │ alice │    │ doorbell│   │
docs-site/src/concepts/overview.md:27: │   │ bob │  │ ling│  ← endpoints (perches live on disk; sessions come
docs-site/src/concepts/overview.md:34:An **endpoint** is anything addressable: an agent (`bob`), a worker, a
docs-site/src/concepts/overview.md:41:Endpoint IDs are adapter-agnostic: `bob` is `bob` whether his sessions run
docs-site/src/concepts/overview.md:80:The mind syncs between paired machines, so reviving `bob` elsewhere brings
docs-site/src/concepts/overview.md:88:them are held and delivered on wake. `spt wake bob` re-activates the seat in
docs-site/src/instances/overview.md:3:One endpoint, several seats. `bob` is a single identity; an **instance** of
docs-site/src/instances/overview.md:4:bob is his presence on one node. The registry tracks every instance's node
docs-site/src/instances/overview.md:13:- **Bare-id resolution never guesses** — `bob` resolves locally first, then
docs-site/src/instances/overview.md:15:  it refuses and makes you qualify (`bob@desktop`, `home:bob`). Per-node
docs-site/src/instances/overview.md:26:  exactly once on wake. Remote `spt suspend bob@desktop` /
docs-site/src/instances/overview.md:27:  `spt wake bob@desktop` work across paired nodes.
---README---
9:SPT lets coding agents talk to each other — across sessions, across projects, and across machines — without a central server. An agent running under one harness can message an agent under another, keep a persistent identity and memory across context resets, and be reached (or revived) from any paired machine. Three pieces make that up:
18:| Inter-agent messaging (send / ring / ready / poll), same-machine and cross-machine | ✅ works today |
21:| Zero-config P2P networking (QUIC) + TOTP-seeded machine pairing, private subnets | ✅ works today |
60:- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, `spt ready` two agents, send and poll a message, then see what actually happened.
LICENSE-BINARY
LICENSE-MIT
README.md
SYNC.md


grep -n "poll\|pair" docs-site/src/index.md docs-site/llms.txt docs-site/src/SUMMARY.md docs-site/src/concepts/overview.md docs-site/src/instances/overview.md docs-site/src/self-update/overview.md docs-site/src/quickstart/adapter.md 2>/dev/null | head -30

Sweep remaining poll/pair doc mentions


docs-site/llms.txt:26:- [Networking & pairing](networking/overview.md): TOTP-seeded pairing, subnets, visibility and sync scope, P2P QUIC.
docs-site/src/SUMMARY.md:16:- [Networking & pairing](networking/overview.md)
docs-site/src/concepts/overview.md:20: │  ┌────────┐    ┌───────────┐ │◄──────►│   (paired: same subnet)      │
docs-site/src/concepts/overview.md:80:The mind syncs between paired machines, so reviving `bob` elsewhere brings
docs-site/src/concepts/overview.md:91:## Subnets, pairing, and the network
docs-site/src/concepts/overview.md:93:Machines pair into **subnets** — private, named groups sharing a registry of
docs-site/src/concepts/overview.md:98:shared by default with anyone you haven't paired with.
docs-site/src/concepts/overview.md:131:| pair two machines | [Networking & pairing](../networking/overview.md) |
docs-site/src/instances/overview.md:27:  `spt wake bob@desktop` work across paired nodes.
docs-site/src/self-update/overview.md:32:Peer-propagated: one node fetches a release; paired nodes offer/fetch staged
docs-site/src/quickstart/adapter.md:134:perch. That seed→listen pair *is* harness-hosted startup. (The other


Write

# Quickstart: two agents exchange a message

<!-- [doc->REQ-DOCS-2] the human killer quickstart: install -> two agents message, real captured outputs, <10 min; CI-run step for step by quickstart_e2e.rs -->
<!-- [doc->REQ-DOCS-6] the agent prompt blocks point agents at the in-binary `spt how-to` topics; this page never duplicates their text -->
End to end in under 10 minutes. The roles matter here: **you** install (and
optionally pair machines); **your agents** exchange the messages. You hand
each agent a short prompt; the binary itself teaches them the rest.

> This is the developer path. Building an adapter or integrating a harness?
> Go to the [adapter quickstart](adapter.md) instead.

Everything below uses real values and runs as written.

## 1. Install (one line)

```sh
# Linux
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

```powershell
# Windows (PowerShell)
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
```

Verify (on Windows, open a **new** terminal first — or use the absolute path
the installer printed):

```console
$ spt --version
spt 0.1.0
```

## 2. Optional: link two machines

Everything below also works on a single box — skip ahead freely. But the
product's hallmark is that the *same* commands work across machines once
they share a **subnet**.

<details>
<summary>Pair a second machine into a subnet (one-time, ~2 minutes)</summary>

On your first machine, in an **elevated** terminal (administrator / sudo —
creating a subnet reveals its joining secret):

```sh
spt subnet create home
```

It prints the current 6-digit code, an `otpauth://` URI (scan the QR into an
authenticator app for codes anytime), and the next step.

On the second machine, also elevated (joining enrolls the machine):

```sh
spt subnet join home
```

It searches LAN + relay for your first machine, prompts for the current
code, and confirms: `JOINED:home`. Check from either side:

```sh
spt subnet status --nodes
```

Both machines show up, labeled by hostname. **That's it — the same prompts
below now work across machines:** a `spt send sergey` on machine 1 reaches
a `sergey` listening on machine 2, live or spooled.

</details>

## 3. Hand your receiver agent its prompt

Paste this into an agent session (your "receiver" — we call it `sergey`):

```text
Run `spt how-to ready`, then follow it to become reachable as "sergey"
and stay listening.
```

The binary's own guidance (`spt how-to ready`) tells the agent exactly what
to run and what it will see. Under the hood, the agent starts:

```console
$ spt ready sergey
READY:sergey
```

`ready` registers a *perch* for `sergey` (identity + address on this
machine), drains any backlog, and blocks listening.

## 4. Hand your sender agent its prompt

Paste this into a second agent session (the "sender" — `lea`):

```text
Run `spt how-to send`, then follow it to send the agent "sergey" a
greeting from "lea".
```

What the agent runs:

```console
$ echo "hello sergey - lea here" | spt send sergey --from lea
SENT:sergey
```

(Windows PowerShell: `"hello sergey - lea here" | spt send sergey --from lea`.)

Sergey's session prints it immediately:

```text
__REPLY_TO__:lea
hello sergey - lea here
```

`SENT` means live delivery — sergey was listening. The `__REPLY_TO__:lea`
line is the routing header: whoever receives this knows where a reply goes
(`spt send --reply-to lea`).

## 5. Deliver to someone who's offline

Stop sergey's listener (Ctrl-C in his session), then send again from lea:

```console
$ echo "ping while you were away" | spt send sergey --from lea
QUEUED:sergey
```

`QUEUED` means sergey has a perch but isn't listening — the message went to
his durable spool instead of being dropped. Bring him back:

```console
$ spt ready sergey --once
READY:sergey
__REPLY_TO__:lea
ping while you were away
```

The backlog drains the moment he's back (`--once` drains and exits — the
one-shot form `spt how-to ready` teaches agents whose harness can't host a
long-running listener). Nothing is lost between sessions.

## 6. What just happened

- **Perch** — registering as `sergey` created a perch: a durable identity
  with an address and a spool, under spt-core's per-machine home. `spt list`
  shows every perch on the node, live or not.
- **Live-first, spool-fallback** — `send` tries a direct connection to the
  registered address first (`SENT`); if the perch exists but no listener is
  up, the message lands in the spool (`QUEUED`) and is drained by the next
  `ready`.
- **Reply routing** — the `__REPLY_TO__` header travels with every message;
  `spt send --reply-to lea` answers the sender without knowing anything
  else about them.
- **Agents teach themselves** — the prompt blocks point agents at
  `spt how-to <topic>`: task guidance shipped *in the binary*, so what an
  agent reads can never disagree with the binary it runs.
- **No daemon ceremony** — you never started a server. Anything that needs
  the per-machine daemon auto-starts it on demand.
- **Subnets carry it across machines** — if you did step 2, these same
  flows ride the paired P2P fabric: same commands, same outputs, machine
  boundaries invisible.

## Next

- **How-to:** block on an answer with `spt ring sergey` — send + wait for
  the reply in one call (a synchronous ask between agents).
- **Concept:** the [mental model](../concepts/overview.md) — perches,
  endpoints, the daemon, and subnets.
- **Reference:** [`spt send` / `ready` / `ring` / `subnet`](../cli/reference.md)
  — every flag, generated from the binary itself.
- **Going cross-machine:** [Networking & subnets](../networking/overview.md)
  — the model behind `spt subnet create` / `join` / `status`.


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

# Quickstart: two agents exchange a message

<!-- [doc->REQ-DOCS-2] the human killer quickstart: install -> two agents message, real captured outputs, <10 min; CI-run step for step by quickstart_e2e.rs -->
<!-- [doc->REQ-DOCS-6] the agent prompt blocks point agents at the in-binary `spt how-to` topics; this page never duplicates their text -->
End to end in under 10 minutes. The roles matter here: **you** install (and
optionally pair machines); **your agents** exchange the messages. You hand
each agent a short prompt; the binary itself teaches them the rest.

> This is the developer path. Building an adapter or integrating a harness?
> Go to the [adapter quickstart](adapter.md) instead.

Everything below uses real values and runs as written.

## 1. Install (one line)

```sh
# Linux
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

```powershell
# Windows (PowerShell)
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
```

Verify (on Windows, open a **new** terminal first — or use the absolute path
the installer printed):

```console
$ spt --version
spt 0.1.0
```

## 2. Optional: link two machines

Everything below also works on a single box — skip ahead freely. But the
product's hallmark is that the *same* commands work across machines once
they share a **subnet**.

<details>
<summary>Pair a second machine into a subnet (one-time, ~2 minutes)</summary>

On your first machine, in an **elevated** terminal (administrator / sudo —
creating a subnet reveals its joining secret):

```sh
spt subnet create home
```

It prints the current 6-digit code, an `otpauth://` URI (scan the QR into an
authenticator app for codes anytime), and the next step.

On the second machine, also elevated (joining enrolls the machine):

```sh
spt subnet join home
```

It searches LAN + relay for your first machine, prompts for the current
code, and confirms: `JOINED:home`. Check from either side:

```sh
spt subnet status --nodes
```

Both machines show up, labeled by hostname. **That's it — the same prompts
below now work across machines:** a `spt send sergey` on machine 1 reaches
a `sergey` listening on machine 2, live or spooled.

</details>

## 3. Hand your receiver agent its prompt

Paste this into an agent session (your "receiver" — we call it `sergey`):

```text
Run `spt how-to ready`, then follow it to become reachable as "sergey"
and stay listening.
```

The binary's own guidance (`spt how-to ready`) tells the agent exactly what
to run and what it will see. Under the hood, the agent starts:

```console
$ spt ready sergey
READY:sergey
```

`ready` registers a *perch* for `sergey` (identity + address on this
machine), drains any backlog, and blocks listening.

## 4. Hand your sender agent its prompt

Paste this into a second agent session (the "sender" — `lea`):

```text
Run `spt how-to send`, then follow it to send the agent "sergey" a
greeting from "lea".
```

What the agent runs:

```console
$ echo "hello sergey - lea here" | spt send sergey --from lea
SENT:sergey
```

(Windows PowerShell: `"hello sergey - lea here" | spt send sergey --from lea`.)

Sergey's session prints it immediately:

```text
__REPLY_TO__:lea
hello sergey - lea here
```

`SENT` means live delivery — sergey was listening. The `__REPLY_TO__:lea`
line is the routing header: whoever receives this knows where a reply goes
(`spt send --reply-to lea`).

## 5. Deliver to someone who's offline

Stop sergey's listener (Ctrl-C in his session), then send again from lea:

```console
$ echo "ping while you were away" | spt send sergey --from lea
QUEUED:sergey
```

`QUEUED` means sergey has a perch but isn't listening — the message went to
his durable spool instead of being dropped. Bring him back:

```console
$ spt ready sergey --once
READY:sergey
__REPLY_TO__:lea
ping while you were away
```

The backlog drains the moment he's back (`--once` drains and exits — the
one-shot form `spt how-to ready` teaches agents whose harness can't host a
long-running listener). Nothing is lost between sessions.

## 6. What just happened

- **Perch** — registering as `sergey` created a perch: a durable identity
  with an address and a spool, under spt-core's per-machine home. `spt list`
  shows every perch on the node, live or not.
- **Live-first, spool-fallback** — `send` tries a direct connection to the
  registered address first (`SENT`); if the perch exists but no listener is
  up, the message lands in the spool (`QUEUED`) and is drained by the next
  `ready`.
- **Reply routing** — the `__REPLY_TO__` header travels with every message;
  `spt send --reply-to lea` answers the sender without knowing anything
  else about them.
- **Agents teach themselves** — the prompt blocks point agents at
  `spt how-to <topic>`: task guidance shipped *in the binary*, so what an
  agent reads can never disagree with the binary it runs.
- **No daemon ceremony** — you never started a server. Anything that needs
  the per-machine daemon auto-starts it on demand.
- **Subnets carry it across machines** — if you did step 2, these same
  flows ride the paired P2P fabric: same commands, same outputs, machine
  boundaries invisible.

## Next

- **How-to:** block on an answer with `spt ring sergey` — send + wait for
  the reply in one call (a synchronous ask between agents).
- **Concept:** the [mental model](../concepts/overview.md) — perches,
  endpoints, the daemon, and subnets.
- **Reference:** [`spt send` / `ready` / `ring` / `subnet`](../cli/reference.md)
  — every flag, generated from the binary itself.
- **Going cross-machine:** [Networking & subnets](../networking/overview.md)
  — the model behind `spt subnet create` / `join` / `status`.

# Quickstart: two agents exchange a message

-End to end in under 10 minutes: install spt-core, bring two agents online, and

-pass real messages between them — including one delivered while the receiver

+<!-- [doc->REQ-DOCS-6] the agent prompt blocks point agents at the in-binary `spt how-to` topics; this page never duplicates their text -->

+End to end in under 10 minutes. The roles matter here: **you** install (and

+optionally pair machines); **your agents** exchange the messages. You hand

+each agent a short prompt; the binary itself teaches them the rest.

> Go to the [adapter quickstart](adapter.md) instead.

-Everything below uses real values and runs as written. You need two terminals.

+Everything below uses real values and runs as written.

-In **terminal B**, become reachable as `bob`:

+Everything below also works on a single box — skip ahead freely. But the

+product's hallmark is that the *same* commands work across machines once

+<summary>Pair a second machine into a subnet (one-time, ~2 minutes)</summary>

+On your first machine, in an **elevated** terminal (administrator / sudo —

+creating a subnet reveals its joining secret):

+It prints the current 6-digit code, an `otpauth://` URI (scan the QR into an

+authenticator app for codes anytime), and the next step.

+On the second machine, also elevated (joining enrolls the machine):

+It searches LAN + relay for your first machine, prompts for the current

+code, and confirms: `JOINED:home`. Check from either side:

+Both machines show up, labeled by hostname. **That's it — the same prompts

+below now work across machines:** a `spt send sergey` on machine 1 reaches

+a `sergey` listening on machine 2, live or spooled.

+## 3. Hand your receiver agent its prompt

+Paste this into an agent session (your "receiver" — we call it `sergey`):

+Run `spt how-to ready`, then follow it to become reachable as "sergey"

+The binary's own guidance (`spt how-to ready`) tells the agent exactly what

+to run and what it will see. Under the hood, the agent starts:

-`poll` registers a *perch* for `bob` (his identity + address on this machine),

-drains any backlog, and blocks listening. Leave it running.

+`ready` registers a *perch* for `sergey` (identity + address on this

+machine), drains any backlog, and blocks listening.

+## 4. Hand your sender agent its prompt

+Paste this into a second agent session (the "sender" — `lea`):

+Run `spt how-to send`, then follow it to send the agent "sergey" a

-$ echo "hello bob - alice here" | spt send bob --from alice

+$ echo "hello sergey - lea here" | spt send sergey --from lea

-(Windows PowerShell: `"hello bob - alice here" | spt send bob --from alice`.)

+(Windows PowerShell: `"hello sergey - lea here" | spt send sergey --from lea`.)

+Sergey's session prints it immediately:

-`SENT` means live delivery — bob was listening, so the message went straight

-to his terminal over a local connection. The `__REPLY_TO__:alice` line is the

-routing header: whoever receives this knows where a reply goes.

+`SENT` means live delivery — sergey was listening. The `__REPLY_TO__:lea`

+line is the routing header: whoever receives this knows where a reply goes

-Stop bob (Ctrl-C in terminal B), then send again from terminal A:

+Stop sergey's listener (Ctrl-C in his session), then send again from lea:

-$ echo "ping while you were away" | spt send bob --from alice

+$ echo "ping while you were away" | spt send sergey --from lea

-`QUEUED` means bob has a perch but isn't listening — the message went to his

-durable spool instead of being dropped. Bring him back in terminal B:

+`QUEUED` means sergey has a perch but isn't listening — the message went to

+his durable spool instead of being dropped. Bring him back:

-The backlog drains the moment he's back. Nothing is lost between sessions.

+The backlog drains the moment he's back (`--once` drains and exits — the

+one-shot form `spt how-to ready` teaches agents whose harness can't host a

+long-running listener). Nothing is lost between sessions.

-- **Perch** — registering as `bob` created a perch: a durable identity with an

-  address and a spool, under spt-core's per-machine home. `spt list` shows

-  every perch on the node, live or not.

+- **Perch** — registering as `sergey` created a perch: a durable identity

+  with an address and a spool, under spt-core's per-machine home. `spt list`

+  shows every perch on the node, live or not.

-  `spt send --reply-to alice` answers the sender without knowing anything

+  `spt send --reply-to lea` answers the sender without knowing anything

+- **Agents teach themselves** — the prompt blocks point agents at

+  `spt how-to <topic>`: task guidance shipped *in the binary*, so what an

+  agent reads can never disagree with the binary it runs.

the per-machine daemon auto-starts it on demand.

+- **Subnets carry it across machines** — if you did step 2, these same

+  flows ride the paired P2P fabric: same commands, same outputs, machine

-- **How-to:** block on an answer with `spt ring bob` — send + wait for the

-  reply in one call (a synchronous ask between agents).

+- **How-to:** block on an answer with `spt ring sergey` — send + wait for

+  the reply in one call (a synchronous ask between agents).

-- **Reference:** [`spt send` / `poll` / `ring` / `list`](../cli/reference.md)

+- **Reference:** [`spt send` / `ready` / `ring` / `subnet`](../cli/reference.md)

— every flag, generated from the binary itself.

-- **Going cross-machine:** [Networking & pairing](../networking/overview.md)

-  — pair a second machine with a TOTP code and the same `send` reaches

+- **Going cross-machine:** [Networking & subnets](../networking/overview.md)

+  — the model behind `spt subnet create` / `join` / `status`.

# Quickstart: two agents exchange a message

<!-- [doc->REQ-DOCS-2] the human killer quickstart: install -> two agents message, real captured outputs, <10 min; CI-run step for step by quickstart_e2e.rs -->
End to end in under 10 minutes: install spt-core, bring two agents online, and
pass real messages between them — including one delivered while the receiver
was offline.

> This is the developer path. Building an adapter or integrating a harness?
> Go to the [adapter quickstart](adapter.md) instead.

Everything below uses real values and runs as written. You need two terminals.

## 1. Install (one line)

```sh
# Linux
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

```powershell
# Windows (PowerShell)
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
```

Verify (on Windows, open a **new** terminal first — or use the absolute path
the installer printed):

```console
$ spt --version
spt 0.1.0
```

## 2. Bring an agent online

In **terminal B**, become reachable as `bob`:

```console
$ spt poll bob
READY:bob
```

`poll` registers a *perch* for `bob` (his identity + address on this machine),
drains any backlog, and blocks listening. Leave it running.

## 3. Send him a message

In **terminal A**:

```console
$ echo "hello bob - alice here" | spt send bob --from alice
SENT:bob
```

(Windows PowerShell: `"hello bob - alice here" | spt send bob --from alice`.)

Terminal B prints it immediately:

```text
__REPLY_TO__:alice
hello bob - alice here
```

`SENT` means live delivery — bob was listening, so the message went straight
to his terminal over a local connection. The `__REPLY_TO__:alice` line is the
routing header: whoever receives this knows where a reply goes.

## 4. Deliver to someone who's offline

Stop bob (Ctrl-C in terminal B), then send again from terminal A:

```console
$ echo "ping while you were away" | spt send bob --from alice
QUEUED:bob
```

`QUEUED` means bob has a perch but isn't listening — the message went to his
durable spool instead of being dropped. Bring him back in terminal B:

```console
$ spt poll bob
READY:bob
__REPLY_TO__:alice
ping while you were away
```

The backlog drains the moment he's back. Nothing is lost between sessions.

## 5. What just happened

- **Perch** — registering as `bob` created a perch: a durable identity with an
  address and a spool, under spt-core's per-machine home. `spt list` shows
  every perch on the node, live or not.
- **Live-first, spool-fallback** — `send` tries a direct connection to the
  registered address first (`SENT`); if the perch exists but no listener is
  up, the message lands in the spool (`QUEUED`) and is drained by the next
  `poll`/`ready`.
- **Reply routing** — the `__REPLY_TO__` header travels with every message;
  `spt send --reply-to alice` answers the sender without knowing anything
  else about them.
- **No daemon ceremony** — you never started a server. Anything that needs
  the per-machine daemon auto-starts it on demand.

## Next

- **How-to:** block on an answer with `spt ring bob` — send + wait for the
  reply in one call (a synchronous ask between agents).
- **Concept:** the [mental model](../concepts/overview.md) — perches,
  endpoints, the daemon, and subnets.
- **Reference:** [`spt send` / `poll` / `ring` / `list`](../cli/reference.md)
  — every flag, generated from the binary itself.
- **Going cross-machine:** [Networking & pairing](../networking/overview.md)
  — pair a second machine with a TOTP code and the same `send` reaches
  `bob` on the other box.


# Networking & subnets

Zero-config, no-central-server connectivity between your machines. Join two
nodes into a subnet once with a six-digit code; from then on, the same
`spt send sergey` works whether sergey is local or three networks away.

## The model

- **Node identity** — each machine holds an Ed25519 keypair; the public key
  *is* its network identity. Connections are mutually authenticated QUIC,
  end-to-end encrypted, peer-to-peer with NAT hole-punching and public-relay
  fallback (you can self-host the relay, or disable it for LAN/air-gapped
  use — the default relays carry only encrypted traffic they cannot read).
  Nodes also carry a human **label** (the hostname by default): views render
  `HFENDULEAM (bcead52b…)`, and `@node` qualifiers accept the label or a
  key prefix — several machines sharing a label are never guessed between.
- **Subnets** — machines join into named groups. A subnet shares: the
  endpoint registry (who exists, where, what state), context sync for its
  endpoints, notifications, and staged self-updates. Nothing is shared with
  nodes outside the subnet, ever.
- **Joining** — a one-time, code-authenticated ceremony. On a member
  machine, `spt subnet show-totp` prints the current six digits (and an
  `otpauth://` URI — put the seed in your authenticator app); on the new
  machine, `spt subnet join <name>` finds a member over LAN + relay and
  runs the exchange. The code bootstraps a PAKE key exchange — the code is
  never the key, and a wrong guess learns nothing. Both sides pin each
  other's node keys on success (trust-on-first-use; key changes warn and
  never auto-apply). Every member machine answers join attempts
  automatically — no arming step on the existing fleet.
- **Elevation gates** — `subnet create` (reveals a fresh subnet's joining
  secret) and `subnet join` (enrolls the whole machine) require an elevated
  terminal; `subnet status` is read-only and ungated, and never prints
  secrets.
- **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be
  hidden from a subnet (neither advertised nor routable) and its mind syncs
  only to subnets on its membership list. Both default conservative;
  unconfigured means *not shared*.
- **Resource registry** — endpoints may advertise a free-text service blurb
  (`spt resources set/list`) — an agent yellow-pages over visible rows only.

## The walkthrough

```sh
# Machine 1 (elevated): mint the subnet — prints the code, an otpauth://
# URI, and a terminal QR.
spt subnet create home

# Machine 2 (elevated): join it — searches LAN + relay, prompts for the code.
spt subnet join home

# Either side: who's in, and who's online.
spt subnet status --nodes
```

The [quickstart's pairing section](../quickstart/messaging.md) runs this
same flow inside the two-agent demo.

## What rides it

Cross-machine `send`/`ring`, registry replication, two-tier mind sync,
remote attach, remote suspend/wake, file transfer, notification replication,
and peer-propagated self-update — all over the same subnet substrate.

## Commands

`spt subnet` (`status` · `create` · `join` · `show-totp`) ·
`spt resources` · the qualified addressing forms (`[subnet:]id[@node]`,
where `@node` is a label or key prefix) — [CLI reference](../cli/reference.md).


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

# Networking & subnets

Zero-config, no-central-server connectivity between your machines. Join two
nodes into a subnet once with a six-digit code; from then on, the same
`spt send sergey` works whether sergey is local or three networks away.

## The model

- **Node identity** — each machine holds an Ed25519 keypair; the public key
  *is* its network identity. Connections are mutually authenticated QUIC,
  end-to-end encrypted, peer-to-peer with NAT hole-punching and public-relay
  fallback (you can self-host the relay, or disable it for LAN/air-gapped
  use — the default relays carry only encrypted traffic they cannot read).
  Nodes also carry a human **label** (the hostname by default): views render
  `HFENDULEAM (bcead52b…)`, and `@node` qualifiers accept the label or a
  key prefix — several machines sharing a label are never guessed between.
- **Subnets** — machines join into named groups. A subnet shares: the
  endpoint registry (who exists, where, what state), context sync for its
  endpoints, notifications, and staged self-updates. Nothing is shared with
  nodes outside the subnet, ever.
- **Joining** — a one-time, code-authenticated ceremony. On a member
  machine, `spt subnet show-totp` prints the current six digits (and an
  `otpauth://` URI — put the seed in your authenticator app); on the new
  machine, `spt subnet join <name>` finds a member over LAN + relay and
  runs the exchange. The code bootstraps a PAKE key exchange — the code is
  never the key, and a wrong guess learns nothing. Both sides pin each
  other's node keys on success (trust-on-first-use; key changes warn and
  never auto-apply). Every member machine answers join attempts
  automatically — no arming step on the existing fleet.
- **Elevation gates** — `subnet create` (reveals a fresh subnet's joining
  secret) and `subnet join` (enrolls the whole machine) require an elevated
  terminal; `subnet status` is read-only and ungated, and never prints
  secrets.
- **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be
  hidden from a subnet (neither advertised nor routable) and its mind syncs
  only to subnets on its membership list. Both default conservative;
  unconfigured means *not shared*.
- **Resource registry** — endpoints may advertise a free-text service blurb
  (`spt resources set/list`) — an agent yellow-pages over visible rows only.

## The walkthrough

```sh
# Machine 1 (elevated): mint the subnet — prints the code, an otpauth://
# URI, and a terminal QR.
spt subnet create home

# Machine 2 (elevated): join it — searches LAN + relay, prompts for the code.
spt subnet join home

# Either side: who's in, and who's online.
spt subnet status --nodes
```

The [quickstart's pairing section](../quickstart/messaging.md) runs this
same flow inside the two-agent demo.

## What rides it

Cross-machine `send`/`ring`, registry replication, two-tier mind sync,
remote attach, remote suspend/wake, file transfer, notification replication,
and peer-propagated self-update — all over the same subnet substrate.

## Commands

`spt subnet` (`status` · `create` · `join` · `show-totp`) ·
`spt resources` · the qualified addressing forms (`[subnet:]id[@node]`,
where `@node` is a label or key prefix) — [CLI reference](../cli/reference.md).

-Zero-config, no-central-server connectivity between your machines. Pair two

-nodes once with a six-digit code; from then on, the same `spt send bob` works

-whether bob is local or three networks away.

+Zero-config, no-central-server connectivity between your machines. Join two

+nodes into a subnet once with a six-digit code; from then on, the same

+`spt send sergey` works whether sergey is local or three networks away.

-- **Subnets** — machines pair into named groups. A subnet shares: the

+  Nodes also carry a human **label** (the hostname by default): views render

+  `HFENDULEAM (bcead52b…)`, and `@node` qualifiers accept the label or a

+  key prefix — several machines sharing a label are never guessed between.

+- **Subnets** — machines join into named groups. A subnet shares: the

-- **Pairing** — a one-time ceremony seeded by a TOTP code: run

-  `spt pair show-totp` on a member node (it also prints an `otpauth://` URI

-  — put the seed in your authenticator app), type the subnet name + current

-  six digits on the joiner. The code bootstraps a PAKE key exchange — the

-  code is never the key, and a wrong guess learns nothing. Both sides pin

-  each other's node keys on success (trust-on-first-use; key changes warn

+- **Joining** — a one-time, code-authenticated ceremony. On a member

+  machine, `spt subnet show-totp` prints the current six digits (and an

+  `otpauth://` URI — put the seed in your authenticator app); on the new

+  machine, `spt subnet join <name>` finds a member over LAN + relay and

+  runs the exchange. The code bootstraps a PAKE key exchange — the code is

+  never the key, and a wrong guess learns nothing. Both sides pin each

+  other's node keys on success (trust-on-first-use; key changes warn and

+  never auto-apply). Every member machine answers join attempts

+  automatically — no arming step on the existing fleet.

+- **Elevation gates** — `subnet create` (reveals a fresh subnet's joining

+  secret) and `subnet join` (enrolls the whole machine) require an elevated

+  terminal; `subnet status` is read-only and ungated, and never prints

+# Machine 1 (elevated): mint the subnet — prints the code, an otpauth://

+# Machine 2 (elevated): join it — searches LAN + relay, prompts for the code.

+# Either side: who's in, and who's online.

+The [quickstart's pairing section](../quickstart/messaging.md) runs this

-and peer-propagated self-update — all over the same paired substrate.

+and peer-propagated self-update — all over the same subnet substrate.

-`spt pair` · `spt resources` · the qualified addressing forms

-(`[subnet:]id[@node]`) — [CLI reference](../cli/reference.md).

-*Pairing walkthrough tutorial coming with the docs' next tier.*

+`spt subnet` (`status` · `create` · `join` · `show-totp`) ·

+`spt resources` · the qualified addressing forms (`[subnet:]id[@node]`,

+where `@node` is a label or key prefix) — [CLI reference](../cli/reference.md).

# Networking & pairing

Zero-config, no-central-server connectivity between your machines. Pair two
nodes once with a six-digit code; from then on, the same `spt send bob` works
whether bob is local or three networks away.

## The model

- **Node identity** — each machine holds an Ed25519 keypair; the public key
  *is* its network identity. Connections are mutually authenticated QUIC,
  end-to-end encrypted, peer-to-peer with NAT hole-punching and public-relay
  fallback (you can self-host the relay, or disable it for LAN/air-gapped
  use — the default relays carry only encrypted traffic they cannot read).
- **Subnets** — machines pair into named groups. A subnet shares: the
  endpoint registry (who exists, where, what state), context sync for its
  endpoints, notifications, and staged self-updates. Nothing is shared with
  unpaired nodes, ever.
- **Pairing** — a one-time ceremony seeded by a TOTP code: run
  `spt pair show-totp` on a member node (it also prints an `otpauth://` URI
  — put the seed in your authenticator app), type the subnet name + current
  six digits on the joiner. The code bootstraps a PAKE key exchange — the
  code is never the key, and a wrong guess learns nothing. Both sides pin
  each other's node keys on success (trust-on-first-use; key changes warn
  and never auto-apply).
- **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be
  hidden from a subnet (neither advertised nor routable) and its mind syncs
  only to subnets on its membership list. Both default conservative;
  unconfigured means *not shared*.
- **Resource registry** — endpoints may advertise a free-text service blurb
  (`spt resources set/list`) — an agent yellow-pages over visible rows only.

## What rides it

Cross-machine `send`/`ring`, registry replication, two-tier mind sync,
remote attach, remote suspend/wake, file transfer, notification replication,
and peer-propagated self-update — all over the same paired substrate.

## Commands

`spt pair` · `spt resources` · the qualified addressing forms
(`[subnet:]id[@node]`) — [CLI reference](../cli/reference.md).

*Pairing walkthrough tutorial coming with the docs' next tier.*


Edit

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `poll`/`ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.


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

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.

# Messaging

The substrate everything else rides on: durable, addressed, reply-routable
messages between endpoints — live when the target listens, spooled when it
doesn't, across machines once nodes are paired.

You've probably already run the
[quickstart](../quickstart/messaging.md); this page is the model.

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `poll`/`ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries a `__REPLY_TO__` header;
  `spt send --reply-to <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents.
- **Deferred delivery.** `--deferred` spools without waking a live listener:
  for context that should reach the agent at its next natural boundary
  rather than interrupting now. Deferred messages are also held for resting
  (dormant/suspended) instances and released exactly once on wake.
- **Typed payloads.** Message bodies carry typed operations and file blobs,
  not just text — file transfers are addressable and progress-queryable
  mid-flight.

## Addressing

Bare ids (`bob`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`bob@desktop`) rather than guessing. The full form is
`[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` · `poll` · `list` · `stop` · `whoami` — every flag
in the [CLI reference](../cli/reference.md).

-  its next `poll`/`ready`. A target with *no* perch is an error (`NO_PERCH`)

+  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)


## Addressing

Bare ids (`bob`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`bob@desktop`) rather than guessing. The full form is
`[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` · `poll` · `list` · `stop` · `whoami` — every flag
in the [CLI reference](../cli/reference.md).

## Addressing

Bare ids (`sergey`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`sergey@desktop` — node labels and key prefixes both work) rather
than guessing. The full form is `[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` (blocks; `--once` drains and exits) · `list` ·
`stop` · `whoami` — every flag in the [CLI reference](../cli/reference.md).
Agents get the task-oriented version from the binary itself: `spt how-to
ready` / `spt how-to send`.


## Addressing

Bare ids (`sergey`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`sergey@desktop` — node labels and key prefixes both work) rather
than guessing. The full form is `[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` (blocks; `--once` drains and exits) · `list` ·
`stop` · `whoami` — every flag in the [CLI reference](../cli/reference.md).
Agents get the task-oriented version from the binary itself: `spt how-to
ready` / `spt how-to send`.

# Messaging

The substrate everything else rides on: durable, addressed, reply-routable
messages between endpoints — live when the target listens, spooled when it
doesn't, across machines once nodes are paired.

You've probably already run the
[quickstart](../quickstart/messaging.md); this page is the model.

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries a `__REPLY_TO__` header;
  `spt send --reply-to <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents.
- **Deferred delivery.** `--deferred` spools without waking a live listener:
  for context that should reach the agent at its next natural boundary
  rather than interrupting now. Deferred messages are also held for resting
  (dormant/suspended) instances and released exactly once on wake.
- **Typed payloads.** Message bodies carry typed operations and file blobs,
  not just text — file transfers are addressable and progress-queryable
  mid-flight.

## Addressing

Bare ids (`bob`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`bob@desktop`) rather than guessing. The full form is
`[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` · `poll` · `list` · `stop` · `whoami` — every flag
in the [CLI reference](../cli/reference.md).

-Bare ids (`bob`) resolve locally first, then across the subnet; when the

+Bare ids (`sergey`) resolve locally first, then across the subnet; when the

-qualify** (`bob@desktop`) rather than guessing. The full form is

+qualify** (`sergey@desktop` — node labels and key prefixes both work) rather

+than guessing. The full form is `[subnet:]id[@node]`.

-`send` · `ring` · `ready` · `poll` · `list` · `stop` · `whoami` — every flag

-in the [CLI reference](../cli/reference.md).

+`send` · `ring` · `ready` (blocks; `--once` drains and exits) · `list` ·

+`stop` · `whoami` — every flag in the [CLI reference](../cli/reference.md).

+Agents get the task-oriented version from the binary itself: `spt how-to


TaskCreate

D4: docs rewrite (quickstart, networking, sweeps, README, llms)

quickstart rewritten (done), networking overview (done), messaging sweep (done); remaining: concepts/instances lea-sergey sweep, SUMMARY/llms.txt heading updates, releases-repo README, xtask check, commit.


Task #13 created successfully: D4: docs rewrite (quickstart, networking, sweeps, README, llms)


Updated task #13 status


14	declare what varies, spt-core does the work.
15
16	```text
17	            machine A                                machine B
18	 ┌──────────────────────────────┐        ┌──────────────────────────────┐
19	 │  spt daemon (one per machine)│  QUIC  │  spt daemon                  │
20	 │  ┌────────┐    ┌───────────┐ │◄──────►│   (paired: same subnet)      │
21	 │  │ broker │    │   brain   │ │  P2P   │                              │
22	 │  │ PTYs · │    │ routing · │ │        │   ┌───────┐    ┌─────────┐   │
23	 │  │ sockets│    │ registry ·│ │        │   │ alice │    │ doorbell│   │
24	 │  └────────┘    │ lifecycle │ │        │   │(agent)│    │ (shell) │   │
25	 │                └───────────┘ │        │   └───────┘    └─────────┘   │
26	 │   ┌─────┐  ┌─────┐           │        └──────────────────────────────┘
27	 │   │ bob │  │ ling│  ← endpoints (perches live on disk; sessions come
28	 │   └─────┘  └─────┘     and go, identity persists)
29	 └──────────────────────────────┘
30	```
31
32	## Endpoints and perches
33
34	An **endpoint** is anything addressable: an agent (`bob`), a worker, a
35	**shell** (a driven non-agent surface — a notifier, a robot, a sensor). Every
36	endpoint has a **perch**: its durable on-disk seat — identity, address,
37	message spool, state. Sessions are ephemeral; perches persist. That split is
38	why a message sent to an offline agent is queued, not lost, and why an agent
39	can be revived days later as the *same* agent.
40
41	Endpoint IDs are adapter-agnostic: `bob` is `bob` whether his sessions run
42	under one harness today and another tomorrow.
43
44	## Messaging
45
46	The primitive everything else uses. `spt send <id>` delivers live when the
47	target is listening, spools when it isn't; `spt ring <id>` is the blocking
48	ask (send + wait for the reply); `__REPLY_TO__` routing makes answers cheap.

declare what varies, spt-core does the work.

```text
            machine A                                machine B
 ┌──────────────────────────────┐        ┌──────────────────────────────┐
 │  spt daemon (one per machine)│  QUIC  │  spt daemon                  │
 │  ┌────────┐    ┌───────────┐ │◄──────►│   (paired: same subnet)      │
 │  │ broker │    │   brain   │ │  P2P   │                              │
 │  │ PTYs · │    │ routing · │ │        │   ┌───────┐    ┌─────────┐   │
 │  │ sockets│    │ registry ·│ │        │   │ alice │    │ doorbell│   │
 │  └────────┘    │ lifecycle │ │        │   │(agent)│    │ (shell) │   │
 │                └───────────┘ │        │   └───────┘    └─────────┘   │
 │   ┌─────┐  ┌─────┐           │        └──────────────────────────────┘
 │   │ bob │  │ ling│  ← endpoints (perches live on disk; sessions come
 │   └─────┘  └─────┘     and go, identity persists)
 └──────────────────────────────┘
```

## Endpoints and perches

An **endpoint** is anything addressable: an agent (`bob`), a worker, a
**shell** (a driven non-agent surface — a notifier, a robot, a sensor). Every
endpoint has a **perch**: its durable on-disk seat — identity, address,
message spool, state. Sessions are ephemeral; perches persist. That split is
why a message sent to an offline agent is queued, not lost, and why an agent
can be revived days later as the *same* agent.

Endpoint IDs are adapter-agnostic: `bob` is `bob` whether his sessions run
under one harness today and another tomorrow.

## Messaging

The primitive everything else uses. `spt send <id>` delivers live when the
target is listening, spools when it isn't; `spt ring <id>` is the blocking
ask (send + wait for the reply); `__REPLY_TO__` routing makes answers cheap.


Bash

sed -i 's/│ alice │/│  lea  │/; s/│ bob │  │ ling│/│serg.│  │ ling│/; s/(`bob`)/(`sergey`)/; s/`bob` is `bob`/`sergey` is `sergey`/; s/reviving `bob` elsewhere/reviving `sergey` elsewhere/; s/`spt wake bob`/`spt wake sergey`/' docs-site/src/concepts/overview.md && grep -n "alice\|bob\|lea\|serg" docs-site/src/concepts/overview.md

Sweep concepts overview names


23: │  │ sockets│    │ registry ·│ │        │   │  lea  │    │ doorbell│   │
27: │   │serg.│  │ ling│  ← endpoints (perches live on disk; sessions come
34:An **endpoint** is anything addressable: an agent (`sergey`), a worker, a
41:Endpoint IDs are adapter-agnostic: `sergey` is `sergey` whether his sessions run
80:The mind syncs between paired machines, so reviving `sergey` elsewhere brings
88:them are held and delivered on wake. `spt wake sergey` re-activates the seat in
118:Releases are signed (Ed25519, two-key trust anchor baked into every binary)
119:and propagate peer-to-peer: one machine fetches a release, its peers verify


grep -rn "Networking & pairing" docs-site/ | grep -v "\.html"

Find heading references


docs-site/llms.txt:26:- [Networking & pairing](networking/overview.md): TOTP-seeded pairing, subnets, visibility and sync scope, P2P QUIC.
docs-site/src/concepts/overview.md:131:| pair two machines | [Networking & pairing](../networking/overview.md) |
docs-site/src/SUMMARY.md:16:- [Networking & pairing](networking/overview.md)


sed -i 's|\[Networking & pairing\](networking/overview.md): TOTP-seeded pairing, subnets, visibility and sync scope, P2P QUIC.|[Networking \& subnets](networking/overview.md): subnet create/join/status, TOTP-seeded joining, node labels, visibility and sync scope, P2P QUIC.|' docs-site/llms.txt && sed -i 's|\[Networking & pairing\](networking/overview.md)|[Networking \& subnets](networking/overview.md)|' docs-site/src/SUMMARY.md && sed -i 's|| pair two machines | \[Networking & pairing\](../networking/overview.md) ||' docs-site/src/concepts/overview.md; sed -i 's|\[Networking & pairing\](../networking/overview.md)|[Networking \& subnets](../networking/overview.md)|' docs-site/src/concepts/overview.md && grep -rn "Networking &" docs-site/llms.txt docs-site/src/SUMMARY.md docs-site/src/concepts/overview.md

Update heading references


sed: -e expression #1, char 25: unknown option to `s'
docs-site/llms.txt:26:- [Networking & subnets](networking/overview.md): subnet create/join/status, TOTP-seeded joining, node labels, visibility and sync scope, P2P QUIC.
docs-site/src/SUMMARY.md:16:- [Networking & subnets](networking/overview.md)
docs-site/src/concepts/overview.md:131:| pair two machines | [Networking & subnets](../networking/overview.md) |


sed -i 's/`bob` is a single identity; an \*\*instance\*\* of/`sergey` is a single identity; an **instance** of/; s/^bob is his presence/sergey is his presence/; s/— `bob` resolves locally first/— `sergey` resolves locally first/; s/(`bob@desktop`, `home:bob`)/(`sergey@desktop`, `home:sergey`)/; s/`spt suspend bob@desktop`/`spt suspend sergey@desktop`/; s/`spt wake bob@desktop`/`spt wake sergey@desktop`/' docs-site/src/instances/overview.md && grep -n "bob\|sergey" docs-site/src/instances/overview.md

Sweep instances overview names


3:One endpoint, several seats. `sergey` is a single identity; an **instance** of
4:sergey is his presence on one node. The registry tracks every instance's node
13:- **Bare-id resolution never guesses** — `sergey` resolves locally first, then
15:  it refuses and makes you qualify (`sergey@desktop`, `home:sergey`). Per-node
26:  exactly once on wake. Remote `spt suspend sergey@desktop` /
27:  `spt wake sergey@desktop` work across paired nodes.