1. agree + possible to target dormant instances by addressing their node i.e. `ling@desktop` 2. agree 3. updating language. default warm (as "dormant"), opt-in auto-cold (as "suspended"), with a subco…


Design spt-core binary and messaging library architecture


CONTEXT is coherent and complete. Assembling the PRD — synthesizing into requirements form, cross-referencing CONTEXT/ADRs rather than re-dumping.


Write

# spt-core — Product Requirements Document

> **Status:** draft, 2026-05-30. Synthesized from a grilling session.
> Canonical detail lives in [`CONTEXT.md`](./CONTEXT.md) (glossary/model) and [`docs/adr/`](./docs/adr/) (decisions). This PRD states *what* and *why*; those state *meaning* and *rationale*. Where they conflict, the ADRs win.

---

## 1. Vision

spt-core is a **harness-independent core for an agent ecosystem**: inter-agent messaging, live-agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).

It is the ground-up rebuild of `claude_skill_owl` ("modern SPT"), untethering the system from Claude Code so that **any** agent runtime — Claude Code, Codex, Cursor, headless, or novel surfaces — can participate by either shelling out to the binary or linking the crates. The first thing built atop it is a rebuilt `spt` plugin reaching parity with today's system; the networking, terminal-wrapper, and multi-instance capabilities then make an agent reachable and operable across all of a user's machines.

## 2. Problem & motivation

- Today's modern SPT works but is **fragile** — each release introduces 2–5 regressions because it grew rapidly from a narrow starting point and carries the resulting tech debt (ADR-0001).
- It is **welded to Claude Code**. Other harnesses can't reuse the messaging/lifecycle substrate.
- Networking, terminal hosting ("capsule"), and cross-machine operation were planned piecemeal atop a CC-tied core; they deserve to be first-class in a clean foundation.

## 3. Goals

1. **Harness independence.** Zero knowledge of any specific agent runtime in core; all harness specifics behind a declarative contract (ADR — runtime model).
2. **Dual deliverable.** First-class library crates *and* a canonical binary, wire-compatible (§Architecture).
3. **Zero-config cross-machine messaging.** "Just works" WAN, baked in day-one (ADR-0002).
4. **One agent, many machines.** The same agent runs natively on each of a user's machines with a synced mind; reachable and operable from anywhere (ADR-0003).
5. **Seamless self-update** with a hard no-endpoint-termination invariant (ADR-0004).
6. **A terminal backbone** that can host sessions headless and headed, locally and over WAN.
7. **Low-friction, secure onboarding** — accountless node pairing (ADR-0005), one-line install, automatic legacy migration.

## 4. Non-goals (v1)

- **macOS support** — out (no test machine); kept structurally easy, not a re-architecture.
- **Cross-subnet** (two *different* users' agents communicating) — deferred concept.
- **Concrete Shells / PresenceChannel implementation / presence gossip** — only the seams ship (§Endpoints).
- **Remote command execution on another user's node** and **instantiate-anywhere** — seam-compatible, deferred behind a consent/security gate.
- **Sidecar adapter protocol**, **manifest includes**, **scrollback disk-spillover**, **predictive/state-sync terminal layer** — see [`docs/DEFERRED.md`](./docs/DEFERRED.md).
- **A model/billing opinion** — SPT is not a harness; model choice is always the adapter's.

## 5. Audience

Single user today (the author); dev-tool audience generally. Agents — not the user — are the primary direct callers of the binary. Nodes are foreseen on novel platforms (Android, Linux handhelds), so packaging must be marketplace-repackaging-friendly.

## 6. Platforms

Windows + Linux for v1. macOS deferred. Pre-Windows-10 (winpty) dropped — ConPTY (Win10+) / `forkpty(3)` Unix via `portable-pty`.

---

## 7. Architecture (requirements)

### 7.1 Workspace & binary
- **R-ARCH-1** Ship a workspace of **many small, acyclically-layered crates** (ADR-0001): `spt-proto` → `spt-store` → `spt-msg` → {`spt-net`, `spt-term`, `spt-runtime`} → `spt-live` → `spt-daemon` → `spt` (binary).
- **R-ARCH-2** Public semver-committed SDK = `spt-proto`, `spt-runtime`, `spt-msg`. All else internal/unstable pre-1.0.
- **R-ARCH-3** `spt-proto` carries an explicit **wire-protocol version** independent of crate semver, with a documented **N-1 compat window**. Binary and library consumers interoperate by proto version, never by release version.
- **R-ARCH-4** Copy-verbatim the commodity layer from the sister project (envelope grammar, spool DDL, registry RPC, `info.json` shape); clean-room only the architecture/lifecycle (ADR-0001).

### 7.2 The daemon (broker/brain)
- **R-DAEMON-1** Exactly **one logical `spt-daemon` per machine**, always-on, owning PTYs, network identity + WAN endpoint, subnet registry, all spools, all poll-listener logic, and all Psyche/pulse loops. No separate listener or wrapper processes (ADR-0004).
- **R-DAEMON-2** Internally split into a stable **broker** (holds PTY masters, harness child processes, listening sockets; versioned IPC; rarely updates) and an **updatable brain** (all logic; rehydrates from disk + re-attaches to broker handles).
- **R-DAEMON-3** Any `spt api` invocation that needs the daemon **auto-starts it** if absent.
- **R-DAEMON-4** Honor every invariant in [`docs/KNOWN-HAZARDS.md`](./docs/KNOWN-HAZARDS.md) as a conformance checklist.

---

## 8. Harness contract (requirements)

The umbrella surface an adapter binds to. Two halves; `SPT is not a harness` — model/billing/harness-env/harness-context are always the adapter's, expressed in its own command templates.

### 8.1 Outbound — the runtime manifest (seams)
- **R-MANIFEST-1** A declarative manifest (TOML/YAML) per adapter, carrying a unified **`adapter_name`**, the adapter's version, and **`min_spt_core_version`** (readable *before* an update is applied — compatibility gate).
- **R-SEAM-spawn** `spawn-session`: command template + `cwd` + `headless?`/`resume?` flags + commune/signoff dirs. spt-core may inject `{id}` and an optional spt-core-generated session UUID. No harness env injection; no initial-context handoff. Optional `id` → reproduces no-id `/spt:live` resolution.
- **R-SEAM-postspawn** `post-spawn`/`api bind`: binary reports `session_id` (if self-generated), `parent_pid`, identity/type, optional HTTP port, **boot nonce**; flips skeleton→live. Skippable *only* under the strict UUID-injection + stable-pid commitment.
- **R-SEAM-psyche** `spawn-psyche`: two templates (fresh / resume-with-`$session_id`), both carrying spt-core-owned `$psyche_prompt`; adapter owns model + harness-specific preamble.
- **R-SEAM-history** Two history paths: **A** adapter-owned locate-template + adapter-owned **normalize-command**; **B** spt-core-native `history-log` store (also used for Shells). Adapter-owned normalize is mandated over built-in parsers (Codex-vs-Claude evidence — transcript formats diverge and move fast).
- **R-SEAM-activity** Activity/idle is reported by the adapter via `api` commands into **perch sentinels** — never PTY-quiescence, never a manifest-declared idle signal.
- **R-SEAM-inject** `inject-input`: configurable per activity-state (activity/idle/both), any combination of PTY injection, `api poll`, in-session relay, or HTTP POST to a local port.
- **R-SEAM-resume** Two resume forms: **fresh-with-preload** (cleared context + `$psyche-context` preload) and **continue-existing** (native harness resume).
- **R-SEAM-capability** Static manifest list of hostable endpoint types (consumed by the registry; a node may host only Shells).
- **R-SEAM-update** `adapter-update`: file-pull (regex + gh repo) or delegated-command. spt-core conducts adapter updates after bootstrap.

### 8.2 Inbound — the `api` subcommand surface
- **R-API-1** All machinery-facing commands are `api`-prefixed; every invocation carries `adapter_name`. The agent-facing verbs (`send`, `ring`, `ready`, …) stay unprefixed.
- **R-API-2** Surface: `api bind`, `api listen` (long-running relay), `api poll [--include-deferred]`, `api state <busy|idle>` (+ `api echo-gate <set|clear>`, `--no-gate`), `api worker-start|worker-stop|worker-poll`, `api boundary <clear|compact>` (carries new `session_id`), `api session-end [--erase]`, `api history-log`, `api presence`, `api seed --pid <pid>`.
- **R-API-3** `commune`/`signoff` are **file-drops**, not commands; the daemon watches the manifest-declared dirs and is the single writer.

### 8.3 Startup flows
- **R-START-1** Adapters never resolve `$SPT_HOME`; install registers the binary dir on system PATH; all bridging is via `spt api`.
- **R-START-2** **Harness-hosted:** SessionStart → `spt api seed --pid` (in-memory seed record) → `$SPT listen <id>` consumes the seed by `parent_pid` (validated vs `session_id`), binds, relays. No file.
- **R-START-3** **spt-hosted:** daemon runs spawn-session into a broker PTY → binary `api bind` (or skips under commitment) → daemon delivers via manifest-configured method. Principle: **file-bridge only when the daemon is not the launcher.**
- **R-START-4** Adapters inject env aliases (`$SPT` = `spt api`, heirs to `$OWL`/`$LIVE`); spt-core supplies the subcommands.

---

## 9. Endpoints, Shells & the Instance model

### 9.1 Endpoint types
- **R-EP-1** Day-one types: **ReadyAgent, LiveAgent, Psyche, Worker, SptNode** (agent/infra), plus the **Shell** family and **PresenceChannel** (seams only — impl deferred). The type system is **open**.
- **R-EP-2** Endpoints split into **agent endpoints** (host an agent) and **Shells** (driven surfaces — nothing intelligent runs there; a remote agent drives them). Shell-vs-agent distinction is present in the type model day-one.
- **R-EP-3** Messaging payloads carry **typed operation commands + arbitrary file blobs** (text/audio/image/video), not just text — the seam that lets Shells, file transfer, and rich PresenceChannel exist later.
- **R-EP-4** PresenceChannel (deferred impl) is a **broker** endpoint with dispatch/bind/thread styles; presence datum `(last_active_node, last_active_endpoint, ts)`.

### 9.2 Instances (ADR-0003, as amended)
- **R-INST-1** Split identity: subnet-wide **endpoint ID** vs per-node **instance**. An instance is the *same* endpoint running **natively** on a machine.
- **R-INST-2** Per-node (anchored): files/project + harness session history. **Synced across instances:** the Psyche mind (two-tier — below).
- **R-INST-3** **Dormant (warm)** is the default resting state (session running, instant wake); **suspended (cold)** is opt-in (session closed, resume-on-wake) and can be triggered from any node. Registry status: active / dormant / suspended / offline.
- **R-INST-4** Active → (dormant|suspended) fires a **transition echo commune** that syncs to the next active instance (catch-up-on-activation).
- **R-INST-5** **Two-tier context sync:** live context → all instances; project context → same-project instances only. (Depends on refining Psyche authoring directives.)
- **R-INST-6** Deferred messages do **not** deliver to dormant/suspended instances until active.
- **R-INST-7** **Subnet registry** (eventually-consistent `endpoint_id → [instances]`) + **resolution policy** (local → most-recently-active → `id@node`) are mandatory v1 foundations.
- **R-INST-8** **Remote-control** (Shell-like attach to an instance on another node; byte-stream terminal over Iroh) is a distinct mode from local operation. Handoff remote→local: `git pull` (files) + fresh-with-preload resume (mind); remote instance goes dormant, no teardown.

### 9.3 Off-node reach-back
- **R-REACH-1** (v1) An agent can detect it is driven remotely (and from which node) and **transfer files** to/from the driving node.
- **R-REACH-2** (deferred) Remote command execution on the user's node — security-gated; workarounds exist via local agents.

---

## 10. Networking & pairing

- **R-NET-1** WAN messaging is first-class day-one, baked into the core crates behind a default-on `net` feature flag (ADR-0002): Iroh + mDNS LAN + pairing.
- **R-NET-2** v1 defaults to **n0.computer relays** (free, accountless) for zero-config WAN, with a **self-hostable-relay** config knob and plain-language disclosure. Traffic is end-to-end encrypted; the dependency is metadata-only and escapable.
- **R-NET-3** **Cross-node Psyche sync** over P2P **replaces** the GitHub-repo sync (no `gh`/account/setup).
- **R-PAIR-1** **TOTP-seeded SPAKE2 pairing** (ADR-0005): the rotating TOTP code is the PAKE password (not a bearer token). The seed is the subnet secret, held by the auth app + all trusted nodes; the user interacts only with the new node.
- **R-PAIR-2** Local trust store (TOFU + warn-on-change). Cross-subnet deferred.
- **R-PAIR-3** Fetch the current pairing code from any paired node (`spt pair --code` or via an agent).
- **R-PAIR-4** Prompt to **name the subnet** on first pairing; show the name on every subsequent pairing.

---

## 11. Self-update

- **R-UPD-1** **Peer-propagated** over P2P (layered on self-fetch; out-of-band still supported); the subnet self-heals to latest (ADR-0004).
- **R-UPD-2** **All binaries signature-verified before handoff**, regardless of source (spt-core release key).
- **R-UPD-3** **Invariant: no endpoint process terminates or suspends during a self-update** — satisfied by the broker/brain split; the broker keeps held PTYs/children/sockets alive across brain replacement.
- **R-UPD-4** **Not auto by default** — gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto.
- **R-UPD-5** spt-core conducts **adapter ripple-updates** after self-update, per each adapter's update declaration and `min_spt_core_version` gate.

---

## 12. Terminal wrapper & frontend

- **R-TERM-1** A process-supervisor terminal wrapper (supersedes "capsule") hosting sessions in broker PTYs, multiplexed and name-addressable, attach/detach, headless↔headed.
- **R-TERM-2** A **session-surface** abstraction with native PTY as the day-one impl and network-attached surfaces for remote control; input injection at both `send-keys` (raw) and `send-line` (cooked) granularity; in-memory scrollback ring (disk spillover deferred).
- **R-TERM-3** Remote terminal streaming uses a **byte-stream** model for v1 (simple, faithful; near-LAN on Iroh-direct paths). Keep the surface abstraction generic enough that a predictive/state-sync "feels-local" layer can be added later without re-architecting — not built v1.
- **R-FRONT-1** Day-one headed frontend is a **launcher/manager** (not raw attach): list running + historic endpoints; launch a historic endpoint from its original manifest; tap into a running one; init a new one via a known adapter. Realizes the "guided resume" (XMB-style) and "management GUI" (per-endpoint panes) sketches.

---

## 13. Installation & migration

- **R-INSTALL-1** Two paths: standalone (primitive) and harness-bootstrapped (calls into standalone). One-line signed-binary install script that registers PATH + the daemon as an OS service (systemd-user / Windows service), with `api`-triggered auto-start fallback.
- **R-INSTALL-2** Marketplace-repackaging-friendly (PortMaster/handheld, Android) — relocatable binary, minimal non-OS-entangled logic.
- **R-INSTALL-3** First run idempotent + interactive-optional: generate node identity, start daemon; pairing + subnet-naming are separate explicit steps.
- **R-MIGRATE-1** Standalone install **auto-detects a `claude_skill_owl` install and offers migration** (identity, agents, tracked Psyche context). First-class supported path.

---

## 14. Project infrastructure
- **R-INFRA-1** Issue/feature tracking on **GitHub** for v1; **tangled.org** documented as the principled migration target.

---

## 15. v1 scope summary

**In v1:** harness-independent core (library + binary); manifest + `api` harness contract; both startup topologies; one-daemon broker/brain; LiveAgent/ReadyAgent/Psyche/Worker/SptNode endpoints + Shell-vs-agent seam + typed/binary payloads; multi-instance data model + subnet registry + resolution policy + dormant/suspended + two-tier context sync + remote-drive of running instances + off-node file transfer; first-class WAN networking + TOTP-SPAKE2 pairing + subnet naming; cross-node Psyche sync (replaces gh-sync); peer-propagated gated signed self-update + adapter ripple; terminal wrapper + launcher frontend (byte-stream remote streaming); one-line install + OS-service + legacy auto-migration; rebuilt spt-plugin to parity.

**Deferred (seam-compatible):** see [`docs/DEFERRED.md`](./docs/DEFERRED.md) — concrete Shells, PresenceChannel impl, presence gossip, instantiate-anywhere + consent, remote command exec + security gate, sidecar adapters, manifest includes, scrollback spillover, predictive/state-sync terminal layer, OS-level presence signal, macOS.

## 16. Success criteria (v1)

1. A non-CC harness (or a headless spike) drives the full live-agent lifecycle through manifest + `api` alone — proving harness independence.
2. The rebuilt spt-plugin reaches **feature parity** with frozen `claude_skill_owl`.
3. Two of a user's machines, freshly paired via one TOTP code, exchange messages and the user drives an agent on machine A from machine B — zero config beyond pairing.
4. A self-update rolls across the subnet **without terminating any endpoint**.
5. The same agent's mind follows the user across machines (two-tier sync); switching machines leaves the prior instance dormant and catches up the new active one.
6. Every `KNOWN-HAZARDS` invariant has a corresponding passing test.

## 17. Open design-phase questions

- Exact `$SPT_HOME` on-disk layout (single source of truth; no flat/nested-sibling ambiguity — KNOWN-HAZARDS 6.1).
- Manifest schema concretes; capability-declaration shape.
- Refining Psyche authoring directives so the live/project context split holds cleanly (R-INST-5).
- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.
- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.
- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.

## 18. References

- [`CONTEXT.md`](./CONTEXT.md) — glossary & model (authoritative for meaning).
- [`docs/adr/0001`](./docs/adr/0001-clean-room-fork-from-claude-skill-owl.md) … [`0005`](./docs/adr/0005-totp-seeded-spake2-node-pairing.md) — decisions.
- [`docs/KNOWN-HAZARDS.md`](./docs/KNOWN-HAZARDS.md) — invariants harvested from the sister project (test checklist).
- [`docs/DEFERRED.md`](./docs/DEFERRED.md) — parked-but-committed features.
- Sister project networking research: `claude_skill_owl/docs/research/SPT Networked Messaging Research Brief.md`.


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

# spt-core — Product Requirements Document

> **Status:** draft, 2026-05-30. Synthesized from a grilling session.
> Canonical detail lives in [`CONTEXT.md`](./CONTEXT.md) (glossary/model) and [`docs/adr/`](./docs/adr/) (decisions). This PRD states *what* and *why*; those state *meaning* and *rationale*. Where they conflict, the ADRs win.

---

## 1. Vision

spt-core is a **harness-independent core for an agent ecosystem**: inter-agent messaging, live-agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).

It is the ground-up rebuild of `claude_skill_owl` ("modern SPT"), untethering the system from Claude Code so that **any** agent runtime — Claude Code, Codex, Cursor, headless, or novel surfaces — can participate by either shelling out to the binary or linking the crates. The first thing built atop it is a rebuilt `spt` plugin reaching parity with today's system; the networking, terminal-wrapper, and multi-instance capabilities then make an agent reachable and operable across all of a user's machines.

## 2. Problem & motivation

- Today's modern SPT works but is **fragile** — each release introduces 2–5 regressions because it grew rapidly from a narrow starting point and carries the resulting tech debt (ADR-0001).
- It is **welded to Claude Code**. Other harnesses can't reuse the messaging/lifecycle substrate.
- Networking, terminal hosting ("capsule"), and cross-machine operation were planned piecemeal atop a CC-tied core; they deserve to be first-class in a clean foundation.

## 3. Goals

1. **Harness independence.** Zero knowledge of any specific agent runtime in core; all harness specifics behind a declarative contract (ADR — runtime model).
2. **Dual deliverable.** First-class library crates *and* a canonical binary, wire-compatible (§Architecture).
3. **Zero-config cross-machine messaging.** "Just works" WAN, baked in day-one (ADR-0002).
4. **One agent, many machines.** The same agent runs natively on each of a user's machines with a synced mind; reachable and operable from anywhere (ADR-0003).
5. **Seamless self-update** with a hard no-endpoint-termination invariant (ADR-0004).
6. **A terminal backbone** that can host sessions headless and headed, locally and over WAN.
7. **Low-friction, secure onboarding** — accountless node pairing (ADR-0005), one-line install, automatic legacy migration.

## 4. Non-goals (v1)

- **macOS support** — out (no test machine); kept structurally easy, not a re-architecture.
- **Cross-subnet** (two *different* users' agents communicating) — deferred concept.
- **Concrete Shells / PresenceChannel implementation / presence gossip** — only the seams ship (§Endpoints).
- **Remote command execution on another user's node** and **instantiate-anywhere** — seam-compatible, deferred behind a consent/security gate.
- **Sidecar adapter protocol**, **manifest includes**, **scrollback disk-spillover**, **predictive/state-sync terminal layer** — see [`docs/DEFERRED.md`](./docs/DEFERRED.md).
- **A model/billing opinion** — SPT is not a harness; model choice is always the adapter's.

## 5. Audience

Single user today (the author); dev-tool audience generally. Agents — not the user — are the primary direct callers of the binary. Nodes are foreseen on novel platforms (Android, Linux handhelds), so packaging must be marketplace-repackaging-friendly.

## 6. Platforms

Windows + Linux for v1. macOS deferred. Pre-Windows-10 (winpty) dropped — ConPTY (Win10+) / `forkpty(3)` Unix via `portable-pty`.

---

## 7. Architecture (requirements)

### 7.1 Workspace & binary
- **R-ARCH-1** Ship a workspace of **many small, acyclically-layered crates** (ADR-0001): `spt-proto` → `spt-store` → `spt-msg` → {`spt-net`, `spt-term`, `spt-runtime`} → `spt-live` → `spt-daemon` → `spt` (binary).
- **R-ARCH-2** Public semver-committed SDK = `spt-proto`, `spt-runtime`, `spt-msg`. All else internal/unstable pre-1.0.
- **R-ARCH-3** `spt-proto` carries an explicit **wire-protocol version** independent of crate semver, with a documented **N-1 compat window**. Binary and library consumers interoperate by proto version, never by release version.
- **R-ARCH-4** Copy-verbatim the commodity layer from the sister project (envelope grammar, spool DDL, registry RPC, `info.json` shape); clean-room only the architecture/lifecycle (ADR-0001).

### 7.2 The daemon (broker/brain)
- **R-DAEMON-1** Exactly **one logical `spt-daemon` per machine**, always-on, owning PTYs, network identity + WAN endpoint, subnet registry, all spools, all poll-listener logic, and all Psyche/pulse loops. No separate listener or wrapper processes (ADR-0004).
- **R-DAEMON-2** Internally split into a stable **broker** (holds PTY masters, harness child processes, listening sockets; versioned IPC; rarely updates) and an **updatable brain** (all logic; rehydrates from disk + re-attaches to broker handles).
- **R-DAEMON-3** Any `spt api` invocation that needs the daemon **auto-starts it** if absent.
- **R-DAEMON-4** Honor every invariant in [`docs/KNOWN-HAZARDS.md`](./docs/KNOWN-HAZARDS.md) as a conformance checklist.

---

## 8. Harness contract (requirements)

The umbrella surface an adapter binds to. Two halves; `SPT is not a harness` — model/billing/harness-env/harness-context are always the adapter's, expressed in its own command templates.

### 8.1 Outbound — the runtime manifest (seams)
- **R-MANIFEST-1** A declarative manifest (TOML/YAML) per adapter, carrying a unified **`adapter_name`**, the adapter's version, and **`min_spt_core_version`** (readable *before* an update is applied — compatibility gate).
- **R-SEAM-spawn** `spawn-session`: command template + `cwd` + `headless?`/`resume?` flags + commune/signoff dirs. spt-core may inject `{id}` and an optional spt-core-generated session UUID. No harness env injection; no initial-context handoff. Optional `id` → reproduces no-id `/spt:live` resolution.
- **R-SEAM-postspawn** `post-spawn`/`api bind`: binary reports `session_id` (if self-generated), `parent_pid`, identity/type, optional HTTP port, **boot nonce**; flips skeleton→live. Skippable *only* under the strict UUID-injection + stable-pid commitment.
- **R-SEAM-psyche** `spawn-psyche`: two templates (fresh / resume-with-`$session_id`), both carrying spt-core-owned `$psyche_prompt`; adapter owns model + harness-specific preamble.
- **R-SEAM-history** Two history paths: **A** adapter-owned locate-template + adapter-owned **normalize-command**; **B** spt-core-native `history-log` store (also used for Shells). Adapter-owned normalize is mandated over built-in parsers (Codex-vs-Claude evidence — transcript formats diverge and move fast).
- **R-SEAM-activity** Activity/idle is reported by the adapter via `api` commands into **perch sentinels** — never PTY-quiescence, never a manifest-declared idle signal.
- **R-SEAM-inject** `inject-input`: configurable per activity-state (activity/idle/both), any combination of PTY injection, `api poll`, in-session relay, or HTTP POST to a local port.
- **R-SEAM-resume** Two resume forms: **fresh-with-preload** (cleared context + `$psyche-context` preload) and **continue-existing** (native harness resume).
- **R-SEAM-capability** Static manifest list of hostable endpoint types (consumed by the registry; a node may host only Shells).
- **R-SEAM-update** `adapter-update`: file-pull (regex + gh repo) or delegated-command. spt-core conducts adapter updates after bootstrap.

### 8.2 Inbound — the `api` subcommand surface
- **R-API-1** All machinery-facing commands are `api`-prefixed; every invocation carries `adapter_name`. The agent-facing verbs (`send`, `ring`, `ready`, …) stay unprefixed.
- **R-API-2** Surface: `api bind`, `api listen` (long-running relay), `api poll [--include-deferred]`, `api state <busy|idle>` (+ `api echo-gate <set|clear>`, `--no-gate`), `api worker-start|worker-stop|worker-poll`, `api boundary <clear|compact>` (carries new `session_id`), `api session-end [--erase]`, `api history-log`, `api presence`, `api seed --pid <pid>`.
- **R-API-3** `commune`/`signoff` are **file-drops**, not commands; the daemon watches the manifest-declared dirs and is the single writer.

### 8.3 Startup flows
- **R-START-1** Adapters never resolve `$SPT_HOME`; install registers the binary dir on system PATH; all bridging is via `spt api`.
- **R-START-2** **Harness-hosted:** SessionStart → `spt api seed --pid` (in-memory seed record) → `$SPT listen <id>` consumes the seed by `parent_pid` (validated vs `session_id`), binds, relays. No file.
- **R-START-3** **spt-hosted:** daemon runs spawn-session into a broker PTY → binary `api bind` (or skips under commitment) → daemon delivers via manifest-configured method. Principle: **file-bridge only when the daemon is not the launcher.**
- **R-START-4** Adapters inject env aliases (`$SPT` = `spt api`, heirs to `$OWL`/`$LIVE`); spt-core supplies the subcommands.

---

## 9. Endpoints, Shells & the Instance model

### 9.1 Endpoint types
- **R-EP-1** Day-one types: **ReadyAgent, LiveAgent, Psyche, Worker, SptNode** (agent/infra), plus the **Shell** family and **PresenceChannel** (seams only — impl deferred). The type system is **open**.
- **R-EP-2** Endpoints split into **agent endpoints** (host an agent) and **Shells** (driven surfaces — nothing intelligent runs there; a remote agent drives them). Shell-vs-agent distinction is present in the type model day-one.
- **R-EP-3** Messaging payloads carry **typed operation commands + arbitrary file blobs** (text/audio/image/video), not just text — the seam that lets Shells, file transfer, and rich PresenceChannel exist later.
- **R-EP-4** PresenceChannel (deferred impl) is a **broker** endpoint with dispatch/bind/thread styles; presence datum `(last_active_node, last_active_endpoint, ts)`.

### 9.2 Instances (ADR-0003, as amended)
- **R-INST-1** Split identity: subnet-wide **endpoint ID** vs per-node **instance**. An instance is the *same* endpoint running **natively** on a machine.
- **R-INST-2** Per-node (anchored): files/project + harness session history. **Synced across instances:** the Psyche mind (two-tier — below).
- **R-INST-3** **Dormant (warm)** is the default resting state (session running, instant wake); **suspended (cold)** is opt-in (session closed, resume-on-wake) and can be triggered from any node. Registry status: active / dormant / suspended / offline.
- **R-INST-4** Active → (dormant|suspended) fires a **transition echo commune** that syncs to the next active instance (catch-up-on-activation).
- **R-INST-5** **Two-tier context sync:** live context → all instances; project context → same-project instances only. (Depends on refining Psyche authoring directives.)
- **R-INST-6** Deferred messages do **not** deliver to dormant/suspended instances until active.
- **R-INST-7** **Subnet registry** (eventually-consistent `endpoint_id → [instances]`) + **resolution policy** (local → most-recently-active → `id@node`) are mandatory v1 foundations.
- **R-INST-8** **Remote-control** (Shell-like attach to an instance on another node; byte-stream terminal over Iroh) is a distinct mode from local operation. Handoff remote→local: `git pull` (files) + fresh-with-preload resume (mind); remote instance goes dormant, no teardown.

### 9.3 Off-node reach-back
- **R-REACH-1** (v1) An agent can detect it is driven remotely (and from which node) and **transfer files** to/from the driving node.
- **R-REACH-2** (deferred) Remote command execution on the user's node — security-gated; workarounds exist via local agents.

---

## 10. Networking & pairing

- **R-NET-1** WAN messaging is first-class day-one, baked into the core crates behind a default-on `net` feature flag (ADR-0002): Iroh + mDNS LAN + pairing.
- **R-NET-2** v1 defaults to **n0.computer relays** (free, accountless) for zero-config WAN, with a **self-hostable-relay** config knob and plain-language disclosure. Traffic is end-to-end encrypted; the dependency is metadata-only and escapable.
- **R-NET-3** **Cross-node Psyche sync** over P2P **replaces** the GitHub-repo sync (no `gh`/account/setup).
- **R-PAIR-1** **TOTP-seeded SPAKE2 pairing** (ADR-0005): the rotating TOTP code is the PAKE password (not a bearer token). The seed is the subnet secret, held by the auth app + all trusted nodes; the user interacts only with the new node.
- **R-PAIR-2** Local trust store (TOFU + warn-on-change). Cross-subnet deferred.
- **R-PAIR-3** Fetch the current pairing code from any paired node (`spt pair --code` or via an agent).
- **R-PAIR-4** Prompt to **name the subnet** on first pairing; show the name on every subsequent pairing.

---

## 11. Self-update

- **R-UPD-1** **Peer-propagated** over P2P (layered on self-fetch; out-of-band still supported); the subnet self-heals to latest (ADR-0004).
- **R-UPD-2** **All binaries signature-verified before handoff**, regardless of source (spt-core release key).
- **R-UPD-3** **Invariant: no endpoint process terminates or suspends during a self-update** — satisfied by the broker/brain split; the broker keeps held PTYs/children/sockets alive across brain replacement.
- **R-UPD-4** **Not auto by default** — gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto.
- **R-UPD-5** spt-core conducts **adapter ripple-updates** after self-update, per each adapter's update declaration and `min_spt_core_version` gate.

---

## 12. Terminal wrapper & frontend

- **R-TERM-1** A process-supervisor terminal wrapper (supersedes "capsule") hosting sessions in broker PTYs, multiplexed and name-addressable, attach/detach, headless↔headed.
- **R-TERM-2** A **session-surface** abstraction with native PTY as the day-one impl and network-attached surfaces for remote control; input injection at both `send-keys` (raw) and `send-line` (cooked) granularity; in-memory scrollback ring (disk spillover deferred).
- **R-TERM-3** Remote terminal streaming uses a **byte-stream** model for v1 (simple, faithful; near-LAN on Iroh-direct paths). Keep the surface abstraction generic enough that a predictive/state-sync "feels-local" layer can be added later without re-architecting — not built v1.
- **R-FRONT-1** Day-one headed frontend is a **launcher/manager** (not raw attach): list running + historic endpoints; launch a historic endpoint from its original manifest; tap into a running one; init a new one via a known adapter. Realizes the "guided resume" (XMB-style) and "management GUI" (per-endpoint panes) sketches.

---

## 13. Installation & migration

- **R-INSTALL-1** Two paths: standalone (primitive) and harness-bootstrapped (calls into standalone). One-line signed-binary install script that registers PATH + the daemon as an OS service (systemd-user / Windows service), with `api`-triggered auto-start fallback.
- **R-INSTALL-2** Marketplace-repackaging-friendly (PortMaster/handheld, Android) — relocatable binary, minimal non-OS-entangled logic.
- **R-INSTALL-3** First run idempotent + interactive-optional: generate node identity, start daemon; pairing + subnet-naming are separate explicit steps.
- **R-MIGRATE-1** Standalone install **auto-detects a `claude_skill_owl` install and offers migration** (identity, agents, tracked Psyche context). First-class supported path.

---

## 14. Project infrastructure
- **R-INFRA-1** Issue/feature tracking on **GitHub** for v1; **tangled.org** documented as the principled migration target.

---

## 15. v1 scope summary

**In v1:** harness-independent core (library + binary); manifest + `api` harness contract; both startup topologies; one-daemon broker/brain; LiveAgent/ReadyAgent/Psyche/Worker/SptNode endpoints + Shell-vs-agent seam + typed/binary payloads; multi-instance data model + subnet registry + resolution policy + dormant/suspended + two-tier context sync + remote-drive of running instances + off-node file transfer; first-class WAN networking + TOTP-SPAKE2 pairing + subnet naming; cross-node Psyche sync (replaces gh-sync); peer-propagated gated signed self-update + adapter ripple; terminal wrapper + launcher frontend (byte-stream remote streaming); one-line install + OS-service + legacy auto-migration; rebuilt spt-plugin to parity.

**Deferred (seam-compatible):** see [`docs/DEFERRED.md`](./docs/DEFERRED.md) — concrete Shells, PresenceChannel impl, presence gossip, instantiate-anywhere + consent, remote command exec + security gate, sidecar adapters, manifest includes, scrollback spillover, predictive/state-sync terminal layer, OS-level presence signal, macOS.

## 16. Success criteria (v1)

1. A non-CC harness (or a headless spike) drives the full live-agent lifecycle through manifest + `api` alone — proving harness independence.
2. The rebuilt spt-plugin reaches **feature parity** with frozen `claude_skill_owl`.
3. Two of a user's machines, freshly paired via one TOTP code, exchange messages and the user drives an agent on machine A from machine B — zero config beyond pairing.
4. A self-update rolls across the subnet **without terminating any endpoint**.
5. The same agent's mind follows the user across machines (two-tier sync); switching machines leaves the prior instance dormant and catches up the new active one.
6. Every `KNOWN-HAZARDS` invariant has a corresponding passing test.

## 17. Open design-phase questions

- Exact `$SPT_HOME` on-disk layout (single source of truth; no flat/nested-sibling ambiguity — KNOWN-HAZARDS 6.1).
- Manifest schema concretes; capability-declaration shape.
- Refining Psyche authoring directives so the live/project context split holds cleanly (R-INST-5).
- Warm-vs-cold dormancy default policy details and the suspend-from-anywhere command surface.
- Consent/security-gate UX (shared by instantiate-anywhere + remote-exec) when those land.
- Whether remote-control surfaces are formally modeled as Shells or as a bare network session-surface.

## 18. References

- [`CONTEXT.md`](./CONTEXT.md) — glossary & model (authoritative for meaning).
- [`docs/adr/0001`](./docs/adr/0001-clean-room-fork-from-claude-skill-owl.md) … [`0005`](./docs/adr/0005-totp-seeded-spake2-node-pairing.md) — decisions.
- [`docs/KNOWN-HAZARDS.md`](./docs/KNOWN-HAZARDS.md) — invariants harvested from the sister project (test checklist).
- [`docs/DEFERRED.md`](./docs/DEFERRED.md) — parked-but-committed features.
- Sister project networking research: `claude_skill_owl/docs/research/SPT Networked Messaging Research Brief.md`.


**PRD assembled** → `spt-core/PRD.md`. The full doc set from this session:

| File | Role |
|---|---|
| **PRD.md** | the deliverable — vision, goals/non-goals, 60+ tagged requirements, v1 scope, success criteria, open questions |
| **CONTEXT.md** | glossary + model, authoritative for meaning (~370 lines) |
| **docs/adr/0001–0005** | clean-room fork · bake-networking/collapse-spt-node · multi-instance (amended) · daemon broker/brain + self-update · TOTP-SPAKE2 pairing |
| **docs/KNOWN-HAZARDS.md** | 23 invariants harvested from the sister project, translated to spt-core's model — v1 test checklist |
| **docs/DEFERRED.md** | 11 parked-but-committed features |

**What got decided (16 forks):** deliverable shape (C) · clean-room hard fork + freeze · `AgentRuntime` trait + manifests · two-way harness contract (manifest seams + `api` surface) · terminal-wrapper supervisor + launcher frontend · networking baked-in / spt-node collapsed · Shell vs agent endpoints + PresenceChannel broker · native multi-instance + dormant/suspended + two-tier mind-sync · peer-propagated gated signed self-update via broker/brain · GitHub→tangled · many-small crates + proto-version · TOTP-seeded SPAKE2 · all 10 outbound seams + full `api` surface + both startup topologies · `api seed`/PATH/daemon-auto-start · install + legacy auto-migration · byte-stream WAN terminal streaming.

**A few things I deliberately left as design-phase open** (in PRD §17), not grilled to ground because they're implementation detail or need their own pass: `$SPT_HOME` on-disk layout, manifest schema concretes, the consent/security-gate UX, and refining the Psyche live/project authoring directives.


<tool_use_error>InputValidationError: SendUserFile failed due to the following issue:
The required parameter `files` is missing</tool_use_error>

InputValidationError: [
  {
    "expected": "array",
    "code": "invalid_type",
    "path": [
      "files"
    ],
    "message": "Invalid input: expected array, received undefined"
  },
  {
    "code": "invalid_value",
    "values": [
      "normal",
      "proactive"
    ],
    "path": [
      "status"
    ],
    "message": "Invalid option: expected one of \"normal\"|\"proactive\""
  }
]


SendUserFile

spt-core PRD — the deliverable. CONTEXT.md + 5 ADRs + KNOWN-HAZARDS + DEFERRED alongside it.


1 file delivered to user.
  C:\Users\decid\Documents\projects\spt-core\PRD.md → file_uuid: 4e792b05-4521-4667-9ca0-2f6caa6df5ea

spt-core PRD — the deliverable. CONTEXT.md + 5 ADRs + KNOWN-HAZARDS + DEFERRED alongside it.


Decision tree resolved end to end; PRD + supporting docs written and delivered.

Want me to keep going on any of the open design-phase items (PRD §17) — most actionable next would be the **`$SPT_HOME` on-disk layout** (single source of truth, no flat/nested ambiguity per KNOWN-HAZARDS 6.1) or the **manifest schema concretes** — or is this a good stopping point to review what's here first?


given how we fleshed out some concepts since they were created, should you audit known hazards, deferred, or ADRs for any oudated points?


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


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


Yes — worth it. We deepened several concepts after those docs were written, so some mappings are now under-specified or risk conflation. Let me audit against the final model.


1	# Deferred Features
2
3	Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.
4
5	| Feature | Cut from | Why deferred | Trigger to revisit |
6	|---|---|---|---|
7	| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
8	| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
9	| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
10	| Concrete Shell types (GameRobot, OS-notification, in-session-inject) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
11	| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1 | Once Shells exist and the subnet registry is stable |
12	| Presence gossip (`last_active_node/endpoint/timestamp` across subnet) | networking v1 | Needs the multi-instance registry first; agent-interaction heartbeat (7a-B) is the chosen signal | When PresenceChannel work begins |
13	| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal | Opt-in enhancement after agent-interaction presence proves out |
14	| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works; consent UX is its own design | After remote-drive ships and consent model is designed |
15	| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |
16

# Deferred Features

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

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, OS-notification, in-session-inject) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1 | Once Shells exist and the subnet registry is stable |
| Presence gossip (`last_active_node/endpoint/timestamp` across subnet) | networking v1 | Needs the multi-instance registry first; agent-interaction heartbeat (7a-B) is the chosen signal | When PresenceChannel work begins |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works; consent UX is its own design | After remote-drive ships and consent model is designed |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |


1	# Bake networking into core; collapse spt-node into the per-machine daemon
2
3	## Status
4
5	accepted (2026-05-28) — reverses the sister project's separate-daemon decision (claude_skill_owl CONTEXT.md §Networking) for the spt-core context.
6
7	## Context
8
9	The sister project (`claude_skill_owl`) decided networking should live in a separate `spt-node` daemon, explicitly to decouple the network stack from SPT's Claude-Code-tied core binary, expose SPT-protocol-as-product, and let networking evolve on its own release cycle. That decision predates spt-core.
10
11	spt-core is a ground-up, harness-independent rebuild. The primary reason `spt-node` was separate — decoupling networking from a CC-tied binary — no longer exists, because spt-core is already decoupled from any harness. Meanwhile the product goal for networking sharpened: WAN cross-machine messaging should be **first-class and zero-config — "just works."**
12
13	## Decision
14
15	Bake the WAN transport stack (Iroh + mDNS LAN discovery + pairing) into spt-core's library crates, behind a compile-time `net` feature flag (default-on in the reference binary, optional for embedded library consumers).
16
17	"Just works WAN, reachable even when no session is running" requires a persistent per-machine network presence — the exact job `spt-node` was created to do. spt-core already commits to a single always-on, one-per-machine `spt-daemon` to supervise PTYs (the terminal wrapper). **Collapse the network-host role into that same daemon (option B1).** One process owns both PTYs and the node identity + WAN endpoint.
18
19	Rejected alternatives:
20	- **Seams-only in core, transport in spt-node (A):** inconsistent with first-class/zero-config networking; preserves a separate-deliverable boundary whose original justification is gone.
21	- **Two daemons — term-daemon + net-daemon (B2):** doubles the always-on process count and the crash/restart/binary-handoff surface for no benefit once both are in core.
22	- **LAN-in-core, WAN-in-node (C):** puts the repo boundary *inside* the transport layer, splitting shared discovery/identity/framing code across two repos.
23
24	**v1 relay stance:** default to n0.computer's free public relays for zero-config onboarding; ship a self-hostable-relay config knob and plain-language disclosure as the principled escape hatch. The dependency is operational, metadata-only (traffic is end-to-end encrypted), and escapable.
25
26	## Consequences
27
28	- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport becomes a later *milestone* of spt-core, not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.
29	- The "second project atop spt-core" is no longer mandated to be spt-node. The real next milestone is the rebuilt spt plugin reaching parity.
30	- The reference binary carries Iroh's footprint (~8MB) and the `noq` git-patch coupling when built with `net`. The feature flag keeps minimal library builds clean.
31	- v1 has a soft, documented runtime dependency on n0-operated relay infrastructure. Acceptable for v1; self-host path is the mitigation.
32	- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles.
33

# Bake networking into core; collapse spt-node into the per-machine daemon

## Status

accepted (2026-05-28) — reverses the sister project's separate-daemon decision (claude_skill_owl CONTEXT.md §Networking) for the spt-core context.

## Context

The sister project (`claude_skill_owl`) decided networking should live in a separate `spt-node` daemon, explicitly to decouple the network stack from SPT's Claude-Code-tied core binary, expose SPT-protocol-as-product, and let networking evolve on its own release cycle. That decision predates spt-core.

spt-core is a ground-up, harness-independent rebuild. The primary reason `spt-node` was separate — decoupling networking from a CC-tied binary — no longer exists, because spt-core is already decoupled from any harness. Meanwhile the product goal for networking sharpened: WAN cross-machine messaging should be **first-class and zero-config — "just works."**

## Decision

Bake the WAN transport stack (Iroh + mDNS LAN discovery + pairing) into spt-core's library crates, behind a compile-time `net` feature flag (default-on in the reference binary, optional for embedded library consumers).

"Just works WAN, reachable even when no session is running" requires a persistent per-machine network presence — the exact job `spt-node` was created to do. spt-core already commits to a single always-on, one-per-machine `spt-daemon` to supervise PTYs (the terminal wrapper). **Collapse the network-host role into that same daemon (option B1).** One process owns both PTYs and the node identity + WAN endpoint.

Rejected alternatives:
- **Seams-only in core, transport in spt-node (A):** inconsistent with first-class/zero-config networking; preserves a separate-deliverable boundary whose original justification is gone.
- **Two daemons — term-daemon + net-daemon (B2):** doubles the always-on process count and the crash/restart/binary-handoff surface for no benefit once both are in core.
- **LAN-in-core, WAN-in-node (C):** puts the repo boundary *inside* the transport layer, splitting shared discovery/identity/framing code across two repos.

**v1 relay stance:** default to n0.computer's free public relays for zero-config onboarding; ship a self-hostable-relay config knob and plain-language disclosure as the principled escape hatch. The dependency is operational, metadata-only (traffic is end-to-end encrypted), and escapable.

## Consequences

- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport becomes a later *milestone* of spt-core, not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.
- The "second project atop spt-core" is no longer mandated to be spt-node. The real next milestone is the rebuilt spt plugin reaching parity.
- The reference binary carries Iroh's footprint (~8MB) and the `noq` git-patch coupling when built with `net`. The feature flag keeps minimal library builds clean.
- v1 has a soft, documented runtime dependency on n0-operated relay infrastructure. Acceptable for v1; self-host path is the mitigation.
- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles.


Found real staleness in ADR-0002. Reading the rest to be thorough.


1	# Clean-room fork from claude_skill_owl with hard freeze on the sister project
2
3	## Status
4
5	accepted (2026-05-28)
6
7	## Context
8
9	spt-core is the rebuild of `claude_skill_owl` (today's modern SPT) into a harness-independent core. The sister project works but is fragile: each feature release introduces 2–5 regressions, often in unprecedented ways, because the substrate grew rapidly from a much narrower starting point and carries the resulting tech debt. spt-core also commits day-one to several architectural shapes (endpoint type system, networking, terminal wrapper as backbone, untethering from Claude Code) that change foundational invariants the sister project assumes.
10
11	## Decision
12
13	Build spt-core as a clean-room fork. Empty workspace, new crate boundaries, fresh lifecycle invariants. Rejected alternatives: rename-and-evolve in-place (preserves the fragility) and a hybrid crate-restructure-with-bulk-port (still drags fragile foundations into the new shape).
14
15	**Mitigations baked in:**
16
17	1. **Copy-verbatim the commodity layer.** Wire formats (`<EVENT>` envelope grammar, TCP registry RPC), the SQLite spool schema/DDL, `info.json` perch metadata shape, and self-contained utility modules are stable and well-tested. Reimplementing them clean-room reintroduces bugs that took months to find in the sister project. Clean-room applies to architecture and lifecycle, not to every artifact.
18
19	2. **Harvest hazards before coding.** Author `KNOWN-HAZARDS.md` from the sister project's invariants and historical bug surface (pid reuse, ephemeral perch lifetimes in `ring`, binary handoff state migration, orphan false-positives during `/clear`, working perch teardown ordering, etc.). It becomes a test checklist for spt-core so rediscovery cost is paid once.
20
21	3. **Hard freeze on claude_skill_owl from 2026-05-28.** Only security and breakage fixes during the rebuild — no new features, no refactors. Soft and no-freeze policies expand the parity moving target indefinitely; hard freeze accepts living with the sister project's current bugs in exchange for finishing.
22
23	## Consequences
24
25	- Dual-maintenance burden is bounded by the freeze, not eliminated. Any breakage fix landed on claude_skill_owl must be considered for porting into spt-core's design (often shape-different, sometimes obsolete).
26	- The sister project keeps shipping until the rebuilt spt plugin (built atop spt-core) reaches feature parity. Until then, the user runs claude_skill_owl in production.
27	- The "feature parity" bar is whatever the sister project does on the freeze date — not a moving target.
28

# Clean-room fork from claude_skill_owl with hard freeze on the sister project

## Status

accepted (2026-05-28)

## Context

spt-core is the rebuild of `claude_skill_owl` (today's modern SPT) into a harness-independent core. The sister project works but is fragile: each feature release introduces 2–5 regressions, often in unprecedented ways, because the substrate grew rapidly from a much narrower starting point and carries the resulting tech debt. spt-core also commits day-one to several architectural shapes (endpoint type system, networking, terminal wrapper as backbone, untethering from Claude Code) that change foundational invariants the sister project assumes.

## Decision

Build spt-core as a clean-room fork. Empty workspace, new crate boundaries, fresh lifecycle invariants. Rejected alternatives: rename-and-evolve in-place (preserves the fragility) and a hybrid crate-restructure-with-bulk-port (still drags fragile foundations into the new shape).

**Mitigations baked in:**

1. **Copy-verbatim the commodity layer.** Wire formats (`<EVENT>` envelope grammar, TCP registry RPC), the SQLite spool schema/DDL, `info.json` perch metadata shape, and self-contained utility modules are stable and well-tested. Reimplementing them clean-room reintroduces bugs that took months to find in the sister project. Clean-room applies to architecture and lifecycle, not to every artifact.

2. **Harvest hazards before coding.** Author `KNOWN-HAZARDS.md` from the sister project's invariants and historical bug surface (pid reuse, ephemeral perch lifetimes in `ring`, binary handoff state migration, orphan false-positives during `/clear`, working perch teardown ordering, etc.). It becomes a test checklist for spt-core so rediscovery cost is paid once.

3. **Hard freeze on claude_skill_owl from 2026-05-28.** Only security and breakage fixes during the rebuild — no new features, no refactors. Soft and no-freeze policies expand the parity moving target indefinitely; hard freeze accepts living with the sister project's current bugs in exchange for finishing.

## Consequences

- Dual-maintenance burden is bounded by the freeze, not eliminated. Any breakage fix landed on claude_skill_owl must be considered for porting into spt-core's design (often shape-different, sometimes obsolete).
- The sister project keeps shipping until the rebuilt spt plugin (built atop spt-core) reaches feature parity. Until then, the user runs claude_skill_owl in production.
- The "feature parity" bar is whatever the sister project does on the freeze date — not a moving target.


1	# Single consolidated daemon with broker/brain split; peer-propagated gated self-update
2
3	## Status
4
5	accepted (2026-05-29)
6
7	## Context
8
9	ADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:
10
11	1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.
12
13	2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive "drain + restart the daemon" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).
14
15	## Decision
16
17	**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.
18
19	**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:
20	- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.
21	- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely; rehydrates from disk state and re-attaches to broker-held handles.
22
23	Routine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.
24
25	Rejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).
26
27	**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.
28
29	**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.
30
31	## Consequences
32
33	- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.
34	- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of "literally one process," preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.
35	- Peer-propagated updates make release signing mandatory, not optional.
36	- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.
37	- The "deliver to most-recently-active session" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.
38	- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.
39

# Single consolidated daemon with broker/brain split; peer-propagated gated self-update

## Status

accepted (2026-05-29)

## Context

ADR-0002 collapsed PTY-hosting and network-hosting into one per-machine `spt-daemon`. Two further forces refine its shape:

1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.

2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive "drain + restart the daemon" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).

## Decision

**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.

**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:
- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.
- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely; rehydrates from disk state and re-attaches to broker-held handles.

Routine self-updates replace only the brain → endpoints never notice. The broker updates rarely (IPC-contract change, held-resource-type change, OS PTY/socket API change, or broker bugfix); the IPC is versioned so a newer brain talks to an older broker. Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is internal, not separately addressable.

Rejected alternatives for the invariant: **drain + restart** (violates no-terminate for spt-hosted sessions); **whole-daemon live FD-passing** (ConPTY handle transfer mid-swap is hard and platform-divergent — pushing that complexity into the rare broker update instead of every routine update is strictly better); **topology-1-only invariant with spt-hosted as fast-follow** (silently breaks the stated hard constraint for the exact new topology spt-core introduces).

**Self-update delivery:** peer-propagated over P2P, layered on self-fetch, out-of-band still supported. All binaries signature-verified before handoff (spt-core's own release key) regardless of source — peer-propagation otherwise lets one compromised node poison the subnet. spt-core conducts updates for the whole stack: self first, then ripple-update each registered adapter via the adapter manifest's update declaration (file-pull or delegated command). The plugin's role shrinks to initial bootstrap only.

**Cadence/consent:** not fully automatic by default; gated on user confirmation delivered to the most-recently-active live session, with an opt-in full-auto choice.

## Consequences

- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.
- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of "literally one process," preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.
- Peer-propagated updates make release signing mandatory, not optional.
- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.
- The "deliver to most-recently-active session" mechanism is a v1 building block that the deferred PresenceChannel will later generalize.
- Whole-daemon live FD-passing (zero-interruption even for broker updates) remains a possible future polish but is explicitly not required for v1.


1	# TOTP-seeded SPAKE2 node pairing
2
3	## Status
4
5	accepted (2026-05-29)
6
7	## Context
8
9	"Zero-config WAN that just works" still requires a first-contact ceremony by which a user's new node is authorized into their existing subnet (key exchange + mutual trust), with no account and no central operator. The research brief vetted Ed25519 identity, Noise, Magic-Wormhole-style PAKE, and TOFU. The product question is the day-one pairing UX for a single user's own machines.
10
11	A natural-seeming option is TOTP: the user provisions a seed into an authenticator app and pairs future nodes with the rotating 6-digit code. Used the obvious way (a new node sends the code to a trusted node for verification), this is insecure: the code is a ~20-bit bearer token, replayable within its 30s window, not bound to the key being exchanged — a real-time MITM can replay the code with their own pubkey and join the subnet. It also performs no key exchange, requiring a separate unbound handshake.
12
13	## Decision
14
15	Adopt **TOTP-seeded SPAKE2 pairing**: keep the familiar TOTP UX but use the 6-digit code as the **password for a SPAKE2 (PAKE) handshake**, not as a verified bearer token.
16
17	- The TOTP **seed** is the durable subnet secret: generated on the first node, shown as a QR for the user's authenticator app, and replicated to every already-trusted node.
18	- To pair a new node, a trusted node (online, no human — it holds the seed) computes the current code as its SPAKE2 password; the user reads the code off their phone and types it into the new node as its password. Matching codes → PAKE succeeds → pubkeys exchanged + bound, MITM-resistant encrypted channel.
19
20	PAKE is the correct primitive for a low-entropy human-transcribed secret: it resists offline brute force (one online guess per attempt) and binds the code to the established key. Cryptographic strength comes from the PAKE construction, not from the 6 digits being secret.
21
22	Rejected alternatives:
23	- **TOTP-as-bearer-token** — replayable, unbound, MITM-vulnerable (above).
24	- **Plain Magic Wormhole / fresh-code-each-time** — secure, but needs a human/relay at *both* ends each pairing. TOTP-seeded PAKE keeps the persistent seed on online trusted nodes so the user interacts only with the new node.
25	- **Paste-the-key** — too error-prone for "just works."
26	- **Account-anchored auto-trust** — reintroduces the account dependency the product is shedding.
27
28	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.
29
30	## Consequences
31
32	- 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.
33	- Operational details to implement: ±1 TOTP window tolerance, handshake-attempt rate limiting, QR re-provisioning for recovery from any still-trusted node.
34	- Losing all nodes *and* the authenticator seed makes the subnet unrecoverable — must be documented.
35	- Revocation of a paired node is a separate trust-store delete; TOTP gates joining only, not continued membership.
36	- Adds a SPAKE2 + TOTP dependency to the core crypto surface (both have mature Rust crates per the research brief).
37

# TOTP-seeded SPAKE2 node pairing

## Status

accepted (2026-05-29)

## Context

"Zero-config WAN that just works" still requires a first-contact ceremony by which a user's new node is authorized into their existing subnet (key exchange + mutual trust), with no account and no central operator. The research brief vetted Ed25519 identity, Noise, Magic-Wormhole-style PAKE, and TOFU. The product question is the day-one pairing UX for a single user's own machines.

A natural-seeming option is TOTP: the user provisions a seed into an authenticator app and pairs future nodes with the rotating 6-digit code. Used the obvious way (a new node sends the code to a trusted node for verification), this is insecure: the code is a ~20-bit bearer token, replayable within its 30s window, not bound to the key being exchanged — a real-time MITM can replay the code with their own pubkey and join the subnet. It also performs no key exchange, requiring a separate unbound handshake.

## Decision

Adopt **TOTP-seeded SPAKE2 pairing**: keep the familiar TOTP UX but use the 6-digit code as the **password for a SPAKE2 (PAKE) handshake**, not as a verified bearer token.

- The TOTP **seed** is the durable subnet secret: generated on the first node, shown as a QR for the user's authenticator app, and replicated to every already-trusted node.
- To pair a new node, a trusted node (online, no human — it holds the seed) computes the current code as its SPAKE2 password; the user reads the code off their phone and types it into the new node as its password. Matching codes → PAKE succeeds → pubkeys exchanged + bound, MITM-resistant encrypted channel.

PAKE is the correct primitive for a low-entropy human-transcribed secret: it resists offline brute force (one online guess per attempt) and binds the code to the established key. Cryptographic strength comes from the PAKE construction, not from the 6 digits being secret.

Rejected alternatives:
- **TOTP-as-bearer-token** — replayable, unbound, MITM-vulnerable (above).
- **Plain Magic Wormhole / fresh-code-each-time** — secure, but needs a human/relay at *both* ends each pairing. TOTP-seeded PAKE keeps the persistent seed on online trusted nodes so the user interacts only with the new node.
- **Paste-the-key** — too error-prone for "just works."
- **Account-anchored auto-trust** — reintroduces the account dependency the product is shedding.

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


1	# Known Hazards
2
3	Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.
4
5	**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.
6
7	---
8
9	## 1. Race conditions & ordering
10
11	### 1.1 Phantom INIT_SIGNOFF after grace period
12	- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.
13	- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.
14	- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.
15	- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.
16
17	### 1.2 Poll-rewrite race & info.json mid-write reads
18	- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.
19	- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).
20	- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.
21	- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.
22
23	### 1.3 Stale `index.lock` wedge from prior git crash
24	- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.
25	- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.
26	- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.
27	- **Sister cite:** CHANGELOG v1.11.20 "Stale `index.lock`"; `src/common/git.rs`.
28
29	### 1.4 Deferred spool rows must not leak to the event stream
30	- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.
31	- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.
32	- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.
33	- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.
34
35	### 1.5 Worker (working-perch) lifecycle path consistency
36	- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.
37	- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.
38	- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.
39	- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.
40
41	---
42
43	## 2. Identity & session-binding
44
45	### 2.1 Parent PID over ephemeral poll PID
46	- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).
47	- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.
48	- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.
49	- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.
50
51	### 2.2 Stdin session_id precedence over env
52	- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.
53	- **Invariant:** stdin-provided session_id wins; env is fallback only.
54	- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.
55	- **Sister cite:** CHANGELOG v1.35.1 "IN-05"; `hook_subagent_start.rs:40-51`.
56
57	### 2.3 Binary-handoff argv schema must stay backward-compatible
58	- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.
59	- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.
60	- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.

# Known Hazards

Hard-won edge cases harvested from the sister project (`claude_skill_owl`, ~80 commits / 12+ phases / multiple production incidents). Per ADR-0001, this is a **test checklist for the spt-core rebuild** — the clean-room rebuild must re-satisfy each invariant rather than re-discover the bug.

**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it differs. Citations point at sister-project paths for reference, not at spt-core.

---

## 1. Race conditions & ordering

### 1.1 Phantom INIT_SIGNOFF after grace period
- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.
- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.
- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.
- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.

### 1.2 Poll-rewrite race & info.json mid-write reads
- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.
- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).
- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.
- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.

### 1.3 Stale `index.lock` wedge from prior git crash
- **Failure:** crashed git leaves a 0-byte `index.lock` in a psyche tracked worktree; every later commit blocks forever.
- **Invariant:** on daemon boot, sweep seed + all agent/project worktrees for stale locks (0 bytes, mtime > 60s) and remove; leave live locks alone.
- **spt-core mapping:** cross-node Psyche sync (ADR-0002/0003) replaces git-repo sync, so the *git* lock may disappear — but any equivalent lockfile in the new sync mechanism needs the same stale-sweep on boot.
- **Sister cite:** CHANGELOG v1.11.20 "Stale `index.lock`"; `src/common/git.rs`.

### 1.4 Deferred spool rows must not leak to the event stream
- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.
- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.
- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.
- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.

### 1.5 Worker (working-perch) lifecycle path consistency
- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.
- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.
- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.
- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.

---

## 2. Identity & session-binding

### 2.1 Parent PID over ephemeral poll PID
- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).
- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.
- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.
- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.

### 2.2 Stdin session_id precedence over env
- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.
- **Invariant:** stdin-provided session_id wins; env is fallback only.
- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.
- **Sister cite:** CHANGELOG v1.35.1 "IN-05"; `hook_subagent_start.rs:40-51`.

### 2.3 Binary-handoff argv schema must stay backward-compatible
- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.
- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.
- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.


Audit complete. KNOWN-HAZARDS, ADR-0001, ADR-0003 (amended), ADR-0004 are all consistent with the final model — KNOWN-HAZARDS was written after the broker/brain ADRs so it already references them. Found genuine staleness in **ADR-0002** (one contradiction) plus two clarity gaps. Fixing.


Edit

- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport becomes a later *milestone* of spt-core, not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.

- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport is a **first-class v1 capability** of spt-core (baked into the core crates, default-on in the reference binary) — *not* a post-v1 milestone and not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.


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

# Bake networking into core; collapse spt-node into the per-machine daemon

## Status

accepted (2026-05-28) — reverses the sister project's separate-daemon decision (claude_skill_owl CONTEXT.md §Networking) for the spt-core context.

## Context

The sister project (`claude_skill_owl`) decided networking should live in a separate `spt-node` daemon, explicitly to decouple the network stack from SPT's Claude-Code-tied core binary, expose SPT-protocol-as-product, and let networking evolve on its own release cycle. That decision predates spt-core.

spt-core is a ground-up, harness-independent rebuild. The primary reason `spt-node` was separate — decoupling networking from a CC-tied binary — no longer exists, because spt-core is already decoupled from any harness. Meanwhile the product goal for networking sharpened: WAN cross-machine messaging should be **first-class and zero-config — "just works."**

## Decision

Bake the WAN transport stack (Iroh + mDNS LAN discovery + pairing) into spt-core's library crates, behind a compile-time `net` feature flag (default-on in the reference binary, optional for embedded library consumers).

"Just works WAN, reachable even when no session is running" requires a persistent per-machine network presence — the exact job `spt-node` was created to do. spt-core already commits to a single always-on, one-per-machine `spt-daemon` to supervise PTYs (the terminal wrapper). **Collapse the network-host role into that same daemon (option B1).** One process owns both PTYs and the node identity + WAN endpoint.

Rejected alternatives:
- **Seams-only in core, transport in spt-node (A):** inconsistent with first-class/zero-config networking; preserves a separate-deliverable boundary whose original justification is gone.
- **Two daemons — term-daemon + net-daemon (B2):** doubles the always-on process count and the crash/restart/binary-handoff surface for no benefit once both are in core.
- **LAN-in-core, WAN-in-node (C):** puts the repo boundary *inside* the transport layer, splitting shared discovery/identity/framing code across two repos.

**v1 relay stance:** default to n0.computer's free public relays for zero-config onboarding; ship a self-hostable-relay config knob and plain-language disclosure as the principled escape hatch. The dependency is operational, metadata-only (traffic is end-to-end encrypted), and escapable.

## Consequences

- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport becomes a later *milestone* of spt-core, not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.
- The "second project atop spt-core" is no longer mandated to be spt-node. The real next milestone is the rebuilt spt plugin reaching parity.
- The reference binary carries Iroh's footprint (~8MB) and the `noq` git-patch coupling when built with `net`. The feature flag keeps minimal library builds clean.
- v1 has a soft, documented runtime dependency on n0-operated relay infrastructure. Acceptable for v1; self-host path is the mitigation.
- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles.

-- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport becomes a later *milestone* of spt-core, not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.

+- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport is a **first-class v1 capability** of spt-core (baked into the core crates, default-on in the reference binary) — *not* a post-v1 milestone and not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.


- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles.

- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles. (Addressed by the broker/brain split in **ADR-0004** — the stable broker holds PTYs + sockets across brain restarts.)


# Bake networking into core; collapse spt-node into the per-machine daemon

## Status

accepted (2026-05-28) — reverses the sister project's separate-daemon decision (claude_skill_owl CONTEXT.md §Networking) for the spt-core context.

## Context

The sister project (`claude_skill_owl`) decided networking should live in a separate `spt-node` daemon, explicitly to decouple the network stack from SPT's Claude-Code-tied core binary, expose SPT-protocol-as-product, and let networking evolve on its own release cycle. That decision predates spt-core.

spt-core is a ground-up, harness-independent rebuild. The primary reason `spt-node` was separate — decoupling networking from a CC-tied binary — no longer exists, because spt-core is already decoupled from any harness. Meanwhile the product goal for networking sharpened: WAN cross-machine messaging should be **first-class and zero-config — "just works."**

## Decision

Bake the WAN transport stack (Iroh + mDNS LAN discovery + pairing) into spt-core's library crates, behind a compile-time `net` feature flag (default-on in the reference binary, optional for embedded library consumers).

"Just works WAN, reachable even when no session is running" requires a persistent per-machine network presence — the exact job `spt-node` was created to do. spt-core already commits to a single always-on, one-per-machine `spt-daemon` to supervise PTYs (the terminal wrapper). **Collapse the network-host role into that same daemon (option B1).** One process owns both PTYs and the node identity + WAN endpoint.

Rejected alternatives:
- **Seams-only in core, transport in spt-node (A):** inconsistent with first-class/zero-config networking; preserves a separate-deliverable boundary whose original justification is gone.
- **Two daemons — term-daemon + net-daemon (B2):** doubles the always-on process count and the crash/restart/binary-handoff surface for no benefit once both are in core.
- **LAN-in-core, WAN-in-node (C):** puts the repo boundary *inside* the transport layer, splitting shared discovery/identity/framing code across two repos.

**v1 relay stance:** default to n0.computer's free public relays for zero-config onboarding; ship a self-hostable-relay config knob and plain-language disclosure as the principled escape hatch. The dependency is operational, metadata-only (traffic is end-to-end encrypted), and escapable.

## Consequences

- `spt-node` is retired from the project vocabulary as a separate deliverable. WAN transport is a **first-class v1 capability** of spt-core (baked into the core crates, default-on in the reference binary) — *not* a post-v1 milestone and not a separate repo. The `SptNode` endpoint type survives unchanged — it remains how a machine is addressed.
- The "second project atop spt-core" is no longer mandated to be spt-node. The real next milestone is the rebuilt spt plugin reaching parity.
- The reference binary carries Iroh's footprint (~8MB) and the `noq` git-patch coupling when built with `net`. The feature flag keeps minimal library builds clean.
- v1 has a soft, documented runtime dependency on n0-operated relay infrastructure. Acceptable for v1; self-host path is the mitigation.
- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles.

-- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles.

+- The single daemon is a single point of failure for both PTY hosting and networking on a machine; its crash-recovery and binary-handoff story must cover both roles. (Addressed by the broker/brain split in **ADR-0004** — the stable broker holds PTYs + sockets across brain restarts.)


| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal | Opt-in enhancement after agent-interaction presence proves out |

| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |


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

# Deferred Features

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

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, OS-notification, in-session-inject) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1 | Once Shells exist and the subnet registry is stable |
| Presence gossip (`last_active_node/endpoint/timestamp` across subnet) | networking v1 | Needs the multi-instance registry first; agent-interaction heartbeat (7a-B) is the chosen signal | When PresenceChannel work begins |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works; consent UX is its own design | After remote-drive ships and consent model is designed |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |

-| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal | Opt-in enhancement after agent-interaction presence proves out |

+| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |