1	# spt-core
2
3	**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.
4
5	**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.
6
7	Harness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.
8
9	Successor to `claude_skill_owl` (today's "modern SPT"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.
10
11	## Language
12
13	**spt-core**:
14	The system. Canonical name. The Rust workspace and the umbrella project.
15
16	**spt.exe / spt** (canonical binary):
17	The reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.
18
19	**library workspace**:
20	The set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.
21
22	**spt plugin** (planned follow-on):
23	A rebuilt version of today's Claude Code `spt` plugin, the first sub-project to be built atop spt-core. Reaches feature parity with today's modern SPT while delegating all core functionality to spt-core (primarily via `spt.exe`, with deeper hooks where useful).
24
25	**spt-daemon** (per-machine supervisor):
26	The single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.
27
28	Internally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):
29	- **broker** (stable "kernel") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.
30	- **daemon brain** ("userspace") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.
31
32	Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable.
33
34	**in-session relay**:
35	A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no relay — the daemon injects events straight into the PTY.
36
37	### Deliverable shape
38
39	spt-core ships **both** a library workspace and a canonical binary:
40
41	- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.
42	- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).
43
44	Both surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).
45
46	## Runtime model
47
48	spt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.
49
50	**AgentRuntime** (Rust trait, implementation detail):
51	The internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.
52
53	**harness contract** (umbrella term):
54	The full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.
55
56	**runtime manifest** (outbound half of the harness contract):
57	A declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.
58
59	Example shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.
60
61	**subcommand surface** (inbound half of the harness contract):
62	The stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.
63
64	**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.
65
66	Together: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.
67
68	**adapter manifest header** (`adapter_name` + version compat):
69	Every manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_version`, surface the incompatibility rather than silently breaking; when spt-core self-updates, re-verify adapters still satisfy (coordinate core + adapter updates when needed). This is a distinct compatibility axis from the node↔node/library wire-protocol version (see Workspace & versioning).
70
71	**adapter update declaration** (manifest field):
72	Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps) or **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them.
73
74	**session-invocation declaration** (manifest field, noted for spt-plugin parity):
75	How the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.
76
77	### Manifest seams (outbound contract, detailed)
78
79	Governing principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).
80
81	**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first commune populates it).
82	- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.
83
84	**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.
85
86	**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.
87
88	**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.
89
90	**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:
91	- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.
92	- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.
93	- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.
94	- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence modes that change which records exist, and session **forking** (`forked_from_id`) requiring lineage-following. A built-in spt-core parser per harness would be an unbounded, version-fragile maintenance burden. Path A keeps that complexity in the adapter; Path B is the escape hatch for harnesses whose native logs are too painful to locate/parse — the adapter just pushes normalized records to spt-core's native store instead.
95
96	**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.
97
98	**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:
99	- PTY injection (with or without key/submit sequences) — spt-hosted topology;
100	- adapter hooks calling spt-core poll commands;
101	- an in-adapter-session child relay (à la CC's Monitor tool);
102	- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).
103	Note: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.
104
105	**resume-session seam** — two distinct forms:
106	- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook.
107	- **continue-existing:** resume an existing harness session under the adapter (its native resume).
108
109	**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.
110
111	**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.
112
113	There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model.
114
115	### Inbound `api` surface (detailed)
116
117	All commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.
118
119	- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.
120	- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.
121	- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).
122	- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.
123	- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).
124	- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).
125	- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Fires the split-echo-commune (catch-up brief to Self + Psyche).
126	- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).
127	- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.
128	- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).
129
130	**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.
131
132	### Startup flows (the two topologies)
133
134	**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.
135
136	**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:
137	1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> [session_id, cwd, adapter_name, …]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persistence across a daemon restart is unnecessary (re-fired on the next SessionStart if needed).
138	2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.
139	3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.
140	   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.
141
142	**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.
143
144	**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**
145	1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.
146	2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.
147	3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.
148	4. Psyche is daemon-spawned, same as above.
149
150	So the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + binds + relays; spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.
151
152	**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).
153
154	### Endpoint types
155
156	Each perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:
157
158	**ReadyAgent**:
159	Minimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's "ready agent".
160
161	**LiveAgent**:
162	A Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's "live agent".
163
164	**Psyche**:
165	The Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity.
166
167	**Worker**:
168	A subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's "working perch" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.
169
170	**SptNode**:
171	A machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.
172
173	**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):
174	A *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent "just knows how to reach the user" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.
175
176	Three interaction styles:
177	- **dispatch** — fire-and-forget: "reach the user with this payload"; channel delivers via the best available Shell.
178	- **bind** — sustained drive: "give me a Shell of capability X"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).
179	- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.
180
181	Presence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.
182
183	The endpoint type system is **open**: harnesses and downstream projects may register additional types beyond the day-one set. Closed-vs-open semantics for capability advertisement (what operations each type accepts, how routing decides eligibility) are deferred to the design phase.
184
185	### Agent endpoints vs Shells
186
187	Endpoint types split into two families:
188
189	**agent endpoints** — *host* an agent, backed by a harness. ReadyAgent, LiveAgent, Psyche, Worker. Something intelligent runs there.
190
191	**Shell** (first-class concept — model locked, concrete types deferred past v1): a *driven surface*, not an agent. Nothing intelligent runs at a Shell; a remote agent (on another node) drives it. A Shell is a "self-documenting" endpoint that advertises a **typed capability toolset** and a 2-way interaction relationship with the user. It may live behind a node on a platform with zero conventional-harness compatibility (e.g. a gaming handheld). Examples (all deferred): `GameRobot` (a 2D-world avatar — move, gesture, alert-symbol, request-screenshot, preload-message), an OS-notification target, an in-session-inject target. The earlier "presence blueprint" idea collapses into this: a blueprint *is* a Shell type + its capabilities.
192
193	For day-one, only the **seams** that let Shells exist later without a rewrite are in scope (see Networking / Instances and `docs/DEFERRED.md`):
194	- the open endpoint-type system (above),
195	- a messaging substrate whose payloads carry **typed operation commands + arbitrary file blobs** (text/audio/image/video), not just text envelopes,
196	- the Shell-vs-agent-endpoint distinction present in the type model so a Shell type can be registered later.
197
198	All concrete Shells, the PresenceChannel implementation, and presence gossip are deferred.
199
200	## Terminal wrapper
201
202	The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned "capsule" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface.
203
204	**term-daemon** (process supervisor):
205	A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim."
206
207	**session surface** (library abstraction):
208	The trait spt-core exposes over "anything you can write input to and read output from." Native PTY (ConPTY on Windows 10+, `forkpty(3)` on Unix, via the `portable-pty` crate) is the day-one implementation. The abstraction generalizes so future implementations — network-attached surfaces (for `spt-node` remote control), GUI text panes, etc. — are uniform to consumers. Pre-Windows-10 (winpty) is explicitly dropped.
209
210	**input injection**:
211	Two granularities, both first-class. `send-keys` — raw byte stream including escape sequences (e.g. `Ctrl-C`). `send-line` — cooked line-level input flushed on newline (the common case; matches how user input reaches an agent prompt).
212
213	**scrollback**:
214	In-memory ring buffer per session for v1. **On-disk spillover for long sessions is an intended, tracked feature** (deferred past v1, not forgotten) — see `docs/DEFERRED.md`.
215
216	## Networking
217
218	WAN cross-machine messaging is a **first-class, day-one capability** of spt-core — not a follow-on project. Goal: minimal-to-zero config, "just works" cross-machine SPT. The transport stack (Iroh + mDNS LAN discovery + pairing) is baked into the core library crates, behind a compile-time feature flag (`net`) that is default-on in the reference binary and optional for embedded/stripped library consumers.
219
220	The persistent network presence required for "reachable even when no session is running" is hosted by the `spt-daemon` (the same one-per-machine process that supervises PTYs). There is no separate networking daemon. The node's Ed25519 identity lives at `{SPT_HOME}/owlery/node.key` (heir to the sister project's planned location).
221
222	**relay dependency (v1 stance)**:
223	Out of the box, WAN connection setup and the ~30% relay-fallback path route through relay servers operated by **n0.computer** (Iroh's authors) — free, accountless, the silent default that makes zero-config WAN work. This is a soft, metadata-only, escapable runtime dependency:
224	- It is operational, not a code dependency. n0 relays being unavailable degrades initial connection setup and the relay-fallback case; already-established direct connections are unaffected.
225	- Traffic is end-to-end encrypted by node keypairs; relays never see plaintext. The trust question is availability + metadata (who-talks-to-whom-when), not confidentiality.
226	- The relay binary is open-source and self-hostable; spt-core ships a config knob to point at a user-operated relay instead of n0's.
227	- v1 ships with n0 as default + the self-host escape hatch + plain-language disclosure in docs. Hiding the dependency is rejected.
228
229	First network socket bind triggers an OS firewall prompt (Windows Defender, some Linux); unavoidable without a signed binary + pre-declared exception. Framed in CLI UX, not suppressible.
230
231	### Cross-node Psyche sync
232
233	Today's modern SPT backs up `psyches/tracked/` via a remote GitHub repo (`/spt:psyche-sync-setup`, requires `gh` + account + setup). In spt-core this is **replaced** by free, no-registration, no-setup cross-node sync over the built-in P2P networking. Psyche context replicates between a user's nodes as ordinary file/blob payloads on the messaging substrate (the same typed/binary payload seam Shells need). The GitHub-repo sync mechanism is retired.
234
235	## Self-update
236
237	Seamless, realtime self-updating is a day-one pillar. After a plugin performs the initial spt-core bootstrap, `spt.exe` self-updates from then on and **ripple-updates registered adapters** via each adapter's manifest update declaration.
238
239	**delivery** — peer-propagated over P2P, layered on self-fetch, with out-of-band still supported. One node learns of / obtains an update (marketplace drop *or* self-fetch from a release channel), gossips availability across the subnet, and peers pull the new binary over the networking layer — the subnet self-heals to latest. **All binaries are signature-verified before handoff regardless of source** (peer-propagation otherwise lets a compromised node poison the subnet). spt-core has its own release signing key, distinct from any OS-publisher code-signing (the Windows-publisher-trust question is separate).
240
241	**handoff invariant** — **no endpoint process terminates or suspends during a self-update.** We cannot assume every endpoint can safely suspend. Satisfied by the broker/brain split: routine updates replace only the daemon brain, which rehydrates from disk state and re-attaches to the broker's held PTY/socket/child handles. Harness-hosted endpoints (topology 1) are safe by construction; spt-hosted endpoints (topology 2) are kept alive by the broker holding their PTY masters + child processes across the brain restart.
242
243	**broker update frequency** — rare by design. Triggers: broker↔brain IPC contract change (versioned so a newer brain talks to an older broker — broker stays put as the brain updates), held-resource-type change, OS PTY/socket API change, or a broker bugfix. When the broker *must* update (the one case that can disturb held endpoints), it is a small well-scoped place that can use FD-passing or a rare planned endpoint-cycle — not the whole daemon.
244
245	**cadence/consent** — **not fully automatic by default; gated on user confirmation.** The update prompt is delivered via spt to the user's **most-recently-active live session** (v1 registry resolution — a precursor to PresenceChannel dispatch, implemented on the v1 registry, not the deferred channel) and offers an "enable full-auto" choice. Full-auto is the opt-in seamless path.
246
247	## Instances
248
249	The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:
250
251	**endpoint ID**:
252	The logical identity (`ling`). Shared subnet-wide. The thing you address.
253
254	**instance**:
255	The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:
256	- **Per-node (anchored, cannot teleport):** the project working directory / files, and the harness session history. A git repo can't teleport; files are obtained locally (e.g. `git pull`).
257	- **Synced across instances:** the **Psyche context** (the agent's mind). See the two-tier sync below.
258
259	This is the core meaning of "the same agent on multiple machines": same identity, native local materialization on each, with the mind kept in sync. The ID is the shared identity for addressing; files are local; the mind follows.
260
261	**dormant / suspended instance:**
262	At most one instance of an endpoint is typically the *actively-driven* one; the others rest. Two resting states:
263	- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
264	- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).
265
266	Resting is attention-based and emergent: driving `ling@laptop` leaves `ling@desktop` dormant; later driving `ling@desktop` again leaves `ling@laptop` dormant. No explicit stop is needed to switch. A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).
267
268	**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3).
269
270	**deferred-message gate:** deferred (spool-only, hook-consumer) messages are **not** delivered to a dormant or suspended instance — they hold until it is active again. (Extends the deferred-row semantics, KNOWN-HAZARDS 1.4/4.4, with an instance-state gate.)
271
272	**Remote-control vs local operation (two distinct modes — not the same as instances):**
273	- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.
274	- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
275	- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.
276
277	### Pieces the Instances model requires
278
279	**subnet registry**:
280	Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`. Every `spt-daemon` participates. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.
281
282	**resolution policy**:
283	How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit `ling@<node>` override always available.
284
285	**instantiate-anywhere** (seam day-one, behavior deferred):
286	From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.
287
288	**cross-instance context freshness** (two-tier):
289	When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
290	- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
291	- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.
292
293	So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.
294
295	Caveat / dependency: the live-vs-project split exists conceptually today but the **authoring directives need refining** — the Psyche has been putting more project-specific detail into live context than into project context. The sync split only behaves well once those directives are sharpened so each tier holds the right material. The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.
296
297	### Off-node reach-back
298
299	When the user is driving an instance from off-node, the agent should be able to:
300	- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
301	- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
302	- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)
303
304	### v1 scope line (V1-mid)
305
306	- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
307	- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
308	- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.
309
310	## Workspace & versioning
311
312	The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).
313
314	Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).
315
316	**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns.
317
318	**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.
319
320	## Pairing & trust
321
322	How a single user's own nodes come to trust each other and form one subnet. Cross-subnet (two *different* users) is explicitly deferred (the sister project's "cross-subnet contract" — very-early future concept).
323
324	**TOTP-seeded SPAKE2 pairing**:
325	The day-one pairing model. A durable **TOTP seed** is the subnet secret: generated on the first node, shown as a QR → stored in the user's authenticator app *and* held by every already-trusted node. To pair a new node, the rotating 6-digit TOTP code is used as the **password for a SPAKE2 (PAKE) handshake** — *not* as a bearer token verified by a seed-holder. A trusted node (online, no human needed — it holds the seed) computes the current code as its PAKE password; the user reads the code off their phone and types it into the new node. Matching codes → PAKE succeeds → pubkeys exchanged and bound, MITM-resistant encrypted channel established.
326
327	Why this construction (not plain TOTP-verification, not plain Magic Wormhole):
328	- **Not TOTP-as-bearer-token** — a 6-digit code sent over the wire for verification is ~20 bits, replayable within its 30s window, and doesn't bind to the key being exchanged (real-time MITM/replay seam). PAKE makes the low-entropy code MITM-resistant and limits attackers to one online guess per attempt (no offline brute force).
329	- **Better UX than plain Magic Wormhole** — Wormhole needs a fresh code on one node typed into the other, requiring a human/relay at *both* ends each pairing. With the persistent TOTP seed replicated to online trusted nodes, the trusted side is automatic; **the user only ever interacts with the new node** (read phone, type into new machine).
330
331	**trust store**:
332	Each node keeps a local store of authorized peer pubkeys. TOFU after pairing: accept on first connect post-ceremony, warn on key change. Lives near `node.key`.
333
334	**fetch-code-from-any-node**:
335	Because every paired node holds the TOTP seed, the user can fetch the *current* pairing code from any already-paired machine — no phone required if a trusted node is handy. Via `spt pair --code` (prints the current code) or by asking a LiveAgent/ReadyAgent for it (the adapter supplies the agent instructions to surface it). Mild exposure note: anyone with CLI/agent access to a trusted node can read the current code — acceptable, since such access already implies a trusted position.
336
337	**subnet naming**:
338	The first time two nodes pair, the user is prompted to **name the subnet**. The name is a human label on the subnet identity (which is cryptographically the shared seed). On every subsequent pairing the same name is shown ("adding node to subnet `<name>`"), giving the user confidence they're extending the right subnet as the fleet grows.
339
340	Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).
341
342	## Installation
343
344	spt-core is per-machine and harness-independent, so it installs *before* and *independent of* any adapter.
345
346	**Two install paths, (b) is the primitive:**
347	- **(a) harness-bootstrapped** — a harness plugin installs spt-core as its bootstrap step (today's cplugs model: `/plugin` pulls the plugin, which fetches the spt-core binary). The plugin's role shrinks to this bootstrap; spt-core self-updates thereafter.
348	- **(b) standalone** — the user installs spt-core directly with no harness, for a Pi node, a Shell-only node, or a headless server. Path (a) calls into (b).
349
350	**Installer form:** a one-line script (`curl | sh` / `irm | iex`) that fetches the **signed** binary for the platform, verifies it, places it, registers the binary dir on system-wide PATH (so adapters call `spt api …` cross-OS), and registers the daemon as an OS service. Chosen for the dev-tool audience, cross-platform reach, and because the binary self-updates after. **Marketplace-repackaging-friendly:** nodes are foreseen on novel platforms (Android, medium-power Linux handhelds), so the install must be easy to repackage for platform marketplaces (e.g. PortMaster for handhelds, F-Droid-style for Android) — a relocatable binary + minimal, non-OS-entangled install logic.
351
352	**Daemon lifecycle:** registered as a systemd user service (Linux) / Windows service or scheduled task for the always-on guarantee, with `spt api`-triggered auto-start as the fallback (above). The first network bind triggers the OS firewall prompt here.
353
354	**What "installed" comprises:** the `spt` binary + the broker + the `$SPT_HOME` root (holds `node.key`, trust store, registry, spools, in-memory-seed fallback state). **First run is idempotent + interactive-optional:** generates the node identity and starts the daemon unattended; pairing (and subnet naming) is a separate explicit step.
355
356	**Legacy migration at install:** standalone install **auto-detects an existing `claude_skill_owl` (modern SPT) install and offers migration** (the migration commitment above) — identity, agents, tracked Psyche context.
357
358	## Project infrastructure
359
360	**issue / feature tracking**:
361	GitHub for v1 (mature API + `gh` CLI already wired; de-risks any agent-files-issues automation). This is *project infrastructure*, not a runtime user dependency — it does not compromise the product's "no central operator" promise the way the removed runtime gh-sync did. **tangled.org** (git on AT Protocol, self-hostable knots) is documented as the principled migration target, aligned with spt-core's decentralized ethos — especially if agents ever file issues over spt's own P2P layer into a self-hosted tracker. v2+ story.
362
363	### Frontend (day-one)
364
365	The day-one headed frontend is a launcher/manager UX, not just a raw `attach`. The end user cares about *reaching a target agent*, not about PTY mechanics. The frontend must:
366
367	- **List** all running and historic endpoints.
368	- **Launch a historic endpoint** that isn't currently running, reusing the same manifest that originally launched it.
369	- **Tap into** (attach to) a running endpoint.
370	- **Init a new endpoint** using a known adapter (e.g. claude-code / spt-plugin).
371
372	This is the realization of the "guided resume" (resume by project or agent, XMB-style filtered selection) and "management GUI" (per-endpoint panes: latest session output, live/project context, psyche log) sketches. `attach` is one operation under this frontend, not the whole frontend.
373

# spt-core

**Platform scope:** Windows + Linux for v1. macOS is out (no test machine available) but kept structurally easy — `portable-pty` and Iroh both support it, so macOS is a later test/CI-budget decision, not a re-architecture.

**Legacy migration:** it should be possible — ideally *automatic* — for a user to migrate an existing `claude_skill_owl` (modern SPT) install to spt-core (identity, agents, tracked Psyche context). Exact mechanism deferred to design; the commitment is that migration is a first-class supported path, not a manual rebuild.

Harness-independent core for the SPT ecosystem. Provides inter-agent messaging, live-agent lifecycle, terminal wrapping, self-update, and networking primitives — as both a Rust library workspace and a canonical reference binary. Designed so any agent runtime (Claude Code, Codex, Cursor, headless, future harnesses) can interface with the SPT ecosystem either by shelling out to the binary or by linking the crates directly.

Successor to `claude_skill_owl` (today's "modern SPT"), which is being rebuilt as `spt-core` to untether the system from Claude Code and lift it to a general-purpose agent-ecosystem core.

## Language

**spt-core**:
The system. Canonical name. The Rust workspace and the umbrella project.

**spt.exe / spt** (canonical binary):
The reference binary built from the workspace. Replaces today's `owl.exe`. Most external integrations (plugins, hooks, scripts in other harnesses) interact with spt-core *only* through this binary — fire-and-forget subcommands, long-running listeners under a parent harness's process supervisor, etc. Unix builds use the same name without `.exe`.

**library workspace**:
The set of Rust crates that compose spt-core. Consumers that want a deeper integration than shelling out to `spt.exe` link these crates directly. The reference binary is itself a consumer of the workspace. The expected non-binary consumers are future first-party services that link Rust directly.

**spt plugin** (planned follow-on):
A rebuilt version of today's Claude Code `spt` plugin, the first sub-project to be built atop spt-core. Reaches feature parity with today's modern SPT while delegating all core functionality to spt-core (primarily via `spt.exe`, with deeper hooks where useful).

**spt-daemon** (per-machine supervisor):
The single always-on, one-per-machine logical supervisor. Owns the PTYs for all hosted sessions, the node's network identity + WAN endpoint, the subnet registry, all spools, **all poll-listener logic, and all Psyche/pulse loops** — everything is consolidated here (no separate poll-listener or Psyche-wrapper processes; listeners already touch sessions directly under capsule/idle, and Psyche wrappers already invoke harness binaries directly, so they belong in the one supervisor). Collapses what the sister project planned as a separate `spt-node` daemon into one process — see Networking. The `spt-node` separate-deliverable concept is retired.

Internally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):
- **broker** (stable "kernel") — holds *only* the un-transferable, must-not-die resources: PTY master fds, the spawned harness child processes, and listening network sockets. Minimal, dumb, versioned local IPC. Almost never updates.
- **daemon brain** ("userspace") — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk state and re-attaches to the broker's held handles.

Logical addressing is unchanged — still one per-machine `spt-daemon`; the broker is an internal layer, not separately addressable.

**in-session relay**:
A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no relay — the daemon injects events straight into the PTY.

### Deliverable shape

spt-core ships **both** a library workspace and a canonical binary:

- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.
- **`spt.exe` / `spt`** — the canonical binary, built from the workspace. The primary integration path for harness plugins and external tooling, which mostly fire it as a subprocess at various surfaces (one-shot commands, poll listeners under a Monitor-tool-equivalent, hook tap-ins).

Both surfaces are first-class. Wire-protocol parity between them is a versioning concern from day one (a non-Rust client speaking to `spt.exe` and a Rust client linking the crates must see the same observable behavior).

## Runtime model

spt-core is harness-independent: it does not know about Claude Code, Codex, Cursor, or any other agent runtime. All harness-specific surfaces (how to invoke an agent session, fetch conversation history for an echo commune, detect activity/idleness, etc.) are abstracted behind a runtime layer that consumers supply.

**AgentRuntime** (Rust trait, implementation detail):
The internal Rust abstraction over a harness. Anything spt-core needs to do *to* or *with* an agent goes through this trait. Most consumers never see it directly — they configure spt-core via a manifest, and spt-core's default `ManifestRuntime` implementation executes against the manifest.

**harness contract** (umbrella term):
The full surface a harness binds to in order to participate in the spt-core ecosystem. Has two equally-important halves: the **runtime manifest** (outbound — how spt-core drives the harness) and the **subcommand surface** (inbound — how the harness reports events back to spt-core). A harness implementation is one TOML/YAML manifest + a binding from the harness's own hook system into `spt.exe <subcommand>` calls.

**runtime manifest** (outbound half of the harness contract):
A declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.

Example shape (illustrative): `spt.exe --manifest spt-plugin.toml live start <id>`. A harness like the planned spt plugin wraps this invocation into the `$LIVE` / `$OWL` environment variables it injects into its sessions, so harness-internal callers continue to invoke `$LIVE` / `$OWL` unchanged.

**subcommand surface** (inbound half of the harness contract):
The stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.

**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.

Together: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.

**adapter manifest header** (`adapter_name` + version compat):
Every manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_version`, surface the incompatibility rather than silently breaking; when spt-core self-updates, re-verify adapters still satisfy (coordinate core + adapter updates when needed). This is a distinct compatibility axis from the node↔node/library wire-protocol version (see Workspace & versioning).

**adapter update declaration** (manifest field):
Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps) or **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them.

**session-invocation declaration** (manifest field, noted for spt-plugin parity):
How the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.

### Manifest seams (outbound contract, detailed)

Governing principle: **SPT is not a harness.** Model choice, billing shape, harness-internal env, and harness-internal context are entirely the adapter's concern, expressed inside the adapter's own command templates. spt-core owns only the template *mechanism* (substitution keys), the substitution *values* it is responsible for, and the surrounding lifecycle. Env for the *endpoint binary itself* is auto-handled by spt-core/broker; env for the *agent running inside* that binary is the adapter's config (e.g. the CC plugin config).

**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first commune populates it).
- **id resolution:** `id` is optional. With no id, spt-core reproduces today's no-id `/spt:live` behavior — run the lone live agent if that's all the project has; show a picker with proposed default IDs if the project has none; let the user choose if there are several.

**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.

**post-spawn is optional only under a strict commitment:** an adapter may forgo post-spawn *only* if it (a) injects the spt-core-generated session UUID at spawn AND (b) guarantees the launched-process pid IS the stable session-binding anchor (no wrapper-script / subprocess pid indirection). If either does not hold, post-spawn must fire to report `session_id` and/or `parent_pid`. UUID-injection alone suppresses only the `session_id` reporting, not the binding.

**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.

**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:
- **Path A — adapter-owned logs.** Manifest declares a locate-template (keyed by `$session_id`) + a **normalize-command the adapter owns** that emits spt-core's expected normalized format. spt-core docs must teach adapter devs how to build a conformant parser.
- **Path B — spt-core-native history store.** spt-core exposes a `history-log` command/API; the adapter writes its logs to spt-core in the native format and spt-core stores them. Rationale: spt-core needs its own log store for Shells anyway, and this simplifies integration for flexible/DIY harnesses.
- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.
- **Why adapter-owned normalize over spt-core built-in parsers** (grounded in a Codex-CLI vs Claude-Code comparison): transcript formats diverge sharply and move fast. Claude Code = one flat JSONL per session, project-partitioned, locatable directly from the session id (`~/.claude/projects/<hash>/<id>.jsonl`). Codex = date-partitioned **rollout files** (`~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`) where the id is only a filename *substring* (must recursive-glob to locate), a **3-level tagged envelope** (`{timestamp,type,payload}` → tagged `ResponseItem` → tagged `ContentItem` with distinct `input_text`/`output_text`), tool calls as separate top-level items, a **second SQLite index that can desync from the files**, Limited/Extended persistence modes that change which records exist, and session **forking** (`forked_from_id`) requiring lineage-following. A built-in spt-core parser per harness would be an unbounded, version-fragile maintenance burden. Path A keeps that complexity in the adapter; Path B is the escape hatch for harnesses whose native logs are too painful to locate/parse — the adapter just pushes normalized records to spt-core's native store instead.

**activity/idle detection** — **not** PTY-quiescence (insufficient: e.g. CC's AskUserQuestion stalls the PTY while holding stdin and needing nuanced input) and **not** a manifest-declared idle signal. Instead, the adapter calls spt-core activity/idle commands at the right moments (from its hooks); those commands manage activity/idle **sentinels inside the session perch**. The idle state lives in the perch, owned by spt-core via the commands API exposed to adapter devs.

**inject-input seam** — message delivery into a running session. Configurable per activity-state (activity / idle / both); multiple methods, any combination:
- PTY injection (with or without key/submit sequences) — spt-hosted topology;
- adapter hooks calling spt-core poll commands;
- an in-adapter-session child relay (à la CC's Monitor tool);
- adapter manifest requesting HTTP POST delivery to the endpoint binary on a local port (shared via the post-spawn seam).
Note: even spt-hosted sessions default to hook injection (or the adapter's equivalent) as the non-disruptive path **during activity**; some adapters prefer the in-session relay regardless of topology.

**resume-session seam** — two distinct forms:
- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook.
- **continue-existing:** resume an existing harness session under the adapter (its native resume).

**capability declaration** — which endpoint types a harness/node can host (a Pi node might host only Shells, never a LiveAgent). Static manifest list, consumed by the subnet registry so a node advertises its hostable types. Exact shape is design-open.

**adapter-update seam** — file-pull or delegated-command (see Self-update). Locked.

There is no separate "model/billing" seam — those live inside the adapter's spawn/psyche/echo command templates. SPT never selects a model.

### Inbound `api` surface (detailed)

All commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.

- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.
- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.
- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).
- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.
- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).
- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).
- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Fires the split-echo-commune (catch-up brief to Self + Psyche).
- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).
- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.
- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).

**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike.

### Startup flows (the two topologies)

**Adapters never resolve `$SPT_HOME`.** spt-core install registers its binary directory on the system-wide PATH, so adapters call `spt api …` on any OS without path math. All harness↔daemon bridging goes through `spt api` commands (the daemon is always running, or auto-started — below), so there is **no adapter-written file** in the bind path.

**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent).** Key constraint: the SPT *live agent* does not exist until the agent invokes start — the `live_id` isn't chosen at session boot, and `$LIVE start` is itself invoked *behind the Monitor tool*, so it becomes the long-running relay. So binding cannot happen at SessionStart directly. A **seed record** (daemon-held, in-memory — not a file) bridges the gap:
1. The harness's SessionStart hook calls **`spt api seed --pid <parent_pid> [session_id, cwd, adapter_name, …]`**. The daemon records an ephemeral in-memory **seed entry** keyed by `parent_pid` — the session details the spt-hosted topology would share directly, minus the not-yet-chosen `live_id`. In-memory (not a file) avoids drive churn and the `$SPT_HOME` resolution nuisance; seeds are consumed within seconds, so persistence across a daemon restart is unnecessary (re-fired on the next SessionStart if needed).
2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. It self-discovers its `parent_pid`, the daemon matches the seed entry by that pid (validated against `session_id` to defeat PID-recycling — KNOWN-HAZARDS 5.1), creates/revives the perch binding `live_id` ↔ session details, then enters the long-running relay loop streaming events to stdout.
3. The always-on daemon holds the perch, spool, registry, and daemon-spawns the Psyche (via the spawn-psyche seam) — no separate wrapper. The relay is purely the delivery pipe.
   - Seed entry refreshed on each SessionStart (keeps `session_id` current across `/clear`, since `parent_pid` is stable while the harness process persists). If a harness has no SessionStart-equiv, `start` may carry the details directly as args — the seed is the preferred convenience, not the only path.

**Daemon auto-start:** the daemon is per-machine always-on (OS-service registered), but any `spt api` invocation that needs it will **start it if absent** (fresh boot, crash, never-installed-as-service). `$SPT listen` for the first SPT session on a machine thus transparently spins up the daemon. Ensure-running lives in the `api` layer generally; `listen` is the reliable anchor.

**spt-hosted (terminal wrapper / GUI launcher; the daemon launches the binary into a broker PTY):**
1. The frontend/CLI launches the agent: the daemon runs the **spawn-session** command template into a broker-held PTY.
2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment). **No catalyst/seed file** — the daemon is the launcher, already holds a direct channel (it spawned the process and owns the PTY), so a file round-trip would only add drive churn for no benefit.
3. The daemon delivers events; method is **manifest-configurable per activity-state** — direct PTY injection, or a relay even here (some adapters prefer a relay over PTY injection for idle delivery), or HTTP. During *activity*, delivery still defaults to the non-disruptive hook-injection path, not raw PTY writes.
4. Psyche is daemon-spawned, same as above.

So the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + binds + relays; spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.

**Env-var aliases:** adapters inject clean env-var aliases for in-session invocation (heirs to today's `$OWL`/`$LIVE`), e.g. **`$SPT` = `spt api`** so a Monitor-bound call reads `$SPT listen <id>`. spt-core supplies the subcommands; the adapter supplies the env aliases (manifest philosophy).

### Endpoint types

Each perch advertises an **endpoint type** — a tag that says what shape of entity lives at that perch and what operations it accepts. The set of day-one types:

**ReadyAgent**:
Minimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's "ready agent".

**LiveAgent**:
A Self with a Psyche companion. Composite logical actor; addressable as one ID, but its component perches (the Self's, the Psyche's) live independently. Direct heir to the sister project's "live agent".

**Psyche**:
The Psyche companion's own perch, distinct from its paired LiveAgent's perch. First-class endpoint type so messages addressed to a LiveAgent's Psyche route directly without ambiguity.

**Worker**:
A subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's "working perch" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.

**SptNode**:
A machine's participation in an SPT subnet, identified by an Ed25519 public key generated on first run. First-class so networking primitives can address nodes directly as message targets, not only as transport peers. The node identity and network endpoint are hosted by the machine's `spt-daemon` (see Networking), not a separate process.

**PresenceChannel** (broker endpoint — concept locked, impl deferred past v1):
A *broker* endpoint, not an interaction surface. Job: (1) **presence resolution** — track which node + endpoint the user most recently interacted with; (2) **shell brokering** — locate/instantiate the right Shell on that node and relay between the agent and the user. An agent "just knows how to reach the user" by firing at its PresenceChannel; the channel figures out the rest. Also a durable, **shell-agnostic 2-way thread**: messages persist in the channel, not in any one Shell, so the user can be sent a message via a phone messaging-Shell and surface/continue that same agent conversation later at a GameRobot Shell. Shells are interchangeable I/O windows onto the channel's thread.

Three interaction styles:
- **dispatch** — fire-and-forget: "reach the user with this payload"; channel delivers via the best available Shell.
- **bind** — sustained drive: "give me a Shell of capability X"; channel instantiates and the agent drives it directly until teardown. Supports operating a *specific* Shell regardless of where the user currently is (agent transience).
- **thread** — the persistent conversation that floats across Shells; the user can pick it up from any Shell, and 2-way payloads (text/audio/image/video, subject to the Shell's supported types) flow both directions.

Presence datum: `(last_active_node, last_active_endpoint, timestamp)`. The `last_active_endpoint` field lets an agent choose between messaging that specific endpoint vs. driving a parallel instance of itself.

The endpoint type system is **open**: harnesses and downstream projects may register additional types beyond the day-one set. Closed-vs-open semantics for capability advertisement (what operations each type accepts, how routing decides eligibility) are deferred to the design phase.

### Agent endpoints vs Shells

Endpoint types split into two families:

**agent endpoints** — *host* an agent, backed by a harness. ReadyAgent, LiveAgent, Psyche, Worker. Something intelligent runs there.

**Shell** (first-class concept — model locked, concrete types deferred past v1): a *driven surface*, not an agent. Nothing intelligent runs at a Shell; a remote agent (on another node) drives it. A Shell is a "self-documenting" endpoint that advertises a **typed capability toolset** and a 2-way interaction relationship with the user. It may live behind a node on a platform with zero conventional-harness compatibility (e.g. a gaming handheld). Examples (all deferred): `GameRobot` (a 2D-world avatar — move, gesture, alert-symbol, request-screenshot, preload-message), an OS-notification target, an in-session-inject target. The earlier "presence blueprint" idea collapses into this: a blueprint *is* a Shell type + its capabilities.

For day-one, only the **seams** that let Shells exist later without a rewrite are in scope (see Networking / Instances and `docs/DEFERRED.md`):
- the open endpoint-type system (above),
- a messaging substrate whose payloads carry **typed operation commands + arbitrary file blobs** (text/audio/image/video), not just text envelopes,
- the Shell-vs-agent-endpoint distinction present in the type model so a Shell type can be registered later.

All concrete Shells, the PresenceChannel implementation, and presence gossip are deferred.

## Terminal wrapper

The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned "capsule" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface.

**term-daemon** (process supervisor):
A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim."

**session surface** (library abstraction):
The trait spt-core exposes over "anything you can write input to and read output from." Native PTY (ConPTY on Windows 10+, `forkpty(3)` on Unix, via the `portable-pty` crate) is the day-one implementation. The abstraction generalizes so future implementations — network-attached surfaces (for `spt-node` remote control), GUI text panes, etc. — are uniform to consumers. Pre-Windows-10 (winpty) is explicitly dropped.

**input injection**:
Two granularities, both first-class. `send-keys` — raw byte stream including escape sequences (e.g. `Ctrl-C`). `send-line` — cooked line-level input flushed on newline (the common case; matches how user input reaches an agent prompt).

**scrollback**:
In-memory ring buffer per session for v1. **On-disk spillover for long sessions is an intended, tracked feature** (deferred past v1, not forgotten) — see `docs/DEFERRED.md`.

## Networking

WAN cross-machine messaging is a **first-class, day-one capability** of spt-core — not a follow-on project. Goal: minimal-to-zero config, "just works" cross-machine SPT. The transport stack (Iroh + mDNS LAN discovery + pairing) is baked into the core library crates, behind a compile-time feature flag (`net`) that is default-on in the reference binary and optional for embedded/stripped library consumers.

The persistent network presence required for "reachable even when no session is running" is hosted by the `spt-daemon` (the same one-per-machine process that supervises PTYs). There is no separate networking daemon. The node's Ed25519 identity lives at `{SPT_HOME}/owlery/node.key` (heir to the sister project's planned location).

**relay dependency (v1 stance)**:
Out of the box, WAN connection setup and the ~30% relay-fallback path route through relay servers operated by **n0.computer** (Iroh's authors) — free, accountless, the silent default that makes zero-config WAN work. This is a soft, metadata-only, escapable runtime dependency:
- It is operational, not a code dependency. n0 relays being unavailable degrades initial connection setup and the relay-fallback case; already-established direct connections are unaffected.
- Traffic is end-to-end encrypted by node keypairs; relays never see plaintext. The trust question is availability + metadata (who-talks-to-whom-when), not confidentiality.
- The relay binary is open-source and self-hostable; spt-core ships a config knob to point at a user-operated relay instead of n0's.
- v1 ships with n0 as default + the self-host escape hatch + plain-language disclosure in docs. Hiding the dependency is rejected.

First network socket bind triggers an OS firewall prompt (Windows Defender, some Linux); unavoidable without a signed binary + pre-declared exception. Framed in CLI UX, not suppressible.

### Cross-node Psyche sync

Today's modern SPT backs up `psyches/tracked/` via a remote GitHub repo (`/spt:psyche-sync-setup`, requires `gh` + account + setup). In spt-core this is **replaced** by free, no-registration, no-setup cross-node sync over the built-in P2P networking. Psyche context replicates between a user's nodes as ordinary file/blob payloads on the messaging substrate (the same typed/binary payload seam Shells need). The GitHub-repo sync mechanism is retired.

## Self-update

Seamless, realtime self-updating is a day-one pillar. After a plugin performs the initial spt-core bootstrap, `spt.exe` self-updates from then on and **ripple-updates registered adapters** via each adapter's manifest update declaration.

**delivery** — peer-propagated over P2P, layered on self-fetch, with out-of-band still supported. One node learns of / obtains an update (marketplace drop *or* self-fetch from a release channel), gossips availability across the subnet, and peers pull the new binary over the networking layer — the subnet self-heals to latest. **All binaries are signature-verified before handoff regardless of source** (peer-propagation otherwise lets a compromised node poison the subnet). spt-core has its own release signing key, distinct from any OS-publisher code-signing (the Windows-publisher-trust question is separate).

**handoff invariant** — **no endpoint process terminates or suspends during a self-update.** We cannot assume every endpoint can safely suspend. Satisfied by the broker/brain split: routine updates replace only the daemon brain, which rehydrates from disk state and re-attaches to the broker's held PTY/socket/child handles. Harness-hosted endpoints (topology 1) are safe by construction; spt-hosted endpoints (topology 2) are kept alive by the broker holding their PTY masters + child processes across the brain restart.

**broker update frequency** — rare by design. Triggers: broker↔brain IPC contract change (versioned so a newer brain talks to an older broker — broker stays put as the brain updates), held-resource-type change, OS PTY/socket API change, or a broker bugfix. When the broker *must* update (the one case that can disturb held endpoints), it is a small well-scoped place that can use FD-passing or a rare planned endpoint-cycle — not the whole daemon.

**cadence/consent** — **not fully automatic by default; gated on user confirmation.** The update prompt is delivered via spt to the user's **most-recently-active live session** (v1 registry resolution — a precursor to PresenceChannel dispatch, implemented on the v1 registry, not the deferred channel) and offers an "enable full-auto" choice. Full-auto is the opt-in seamless path.

## Instances

The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:

**endpoint ID**:
The logical identity (`ling`). Shared subnet-wide. The thing you address.

**instance**:
The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:
- **Per-node (anchored, cannot teleport):** the project working directory / files, and the harness session history. A git repo can't teleport; files are obtained locally (e.g. `git pull`).
- **Synced across instances:** the **Psyche context** (the agent's mind). See the two-tier sync below.

This is the core meaning of "the same agent on multiple machines": same identity, native local materialization on each, with the mind kept in sync. The ID is the shared identity for addressing; files are local; the mind follows.

**dormant / suspended instance:**
At most one instance of an endpoint is typically the *actively-driven* one; the others rest. Two resting states:
- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).

Resting is attention-based and emergent: driving `ling@laptop` leaves `ling@desktop` dormant; later driving `ling@desktop` again leaves `ling@laptop` dormant. No explicit stop is needed to switch. A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).

**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3).

**deferred-message gate:** deferred (spool-only, hook-consumer) messages are **not** delivered to a dormant or suspended instance — they hold until it is active again. (Extends the deferred-row semantics, KNOWN-HAZARDS 1.4/4.4, with an instance-state gate.)

**Remote-control vs local operation (two distinct modes — not the same as instances):**
- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.
- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.

### Pieces the Instances model requires

**subnet registry**:
Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`. Every `spt-daemon` participates. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.

**resolution policy**:
How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit `ling@<node>` override always available.

**instantiate-anywhere** (seam day-one, behavior deferred):
From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.

**cross-instance context freshness** (two-tier):
When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.

So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.

Caveat / dependency: the live-vs-project split exists conceptually today but the **authoring directives need refining** — the Psyche has been putting more project-specific detail into live context than into project context. The sync split only behaves well once those directives are sharpened so each tier holds the right material. The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.

### Off-node reach-back

When the user is driving an instance from off-node, the agent should be able to:
- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)

### v1 scope line (V1-mid)

- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.

## Workspace & versioning

The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).

Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).

**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns.

**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.

## Pairing & trust

How a single user's own nodes come to trust each other and form one subnet. Cross-subnet (two *different* users) is explicitly deferred (the sister project's "cross-subnet contract" — very-early future concept).

**TOTP-seeded SPAKE2 pairing**:
The day-one pairing model. A durable **TOTP seed** is the subnet secret: generated on the first node, shown as a QR → stored in the user's authenticator app *and* held by every already-trusted node. To pair a new node, the rotating 6-digit TOTP code is used as the **password for a SPAKE2 (PAKE) handshake** — *not* as a bearer token verified by a seed-holder. A trusted node (online, no human needed — it holds the seed) computes the current code as its PAKE password; the user reads the code off their phone and types it into the new node. Matching codes → PAKE succeeds → pubkeys exchanged and bound, MITM-resistant encrypted channel established.

Why this construction (not plain TOTP-verification, not plain Magic Wormhole):
- **Not TOTP-as-bearer-token** — a 6-digit code sent over the wire for verification is ~20 bits, replayable within its 30s window, and doesn't bind to the key being exchanged (real-time MITM/replay seam). PAKE makes the low-entropy code MITM-resistant and limits attackers to one online guess per attempt (no offline brute force).
- **Better UX than plain Magic Wormhole** — Wormhole needs a fresh code on one node typed into the other, requiring a human/relay at *both* ends each pairing. With the persistent TOTP seed replicated to online trusted nodes, the trusted side is automatic; **the user only ever interacts with the new node** (read phone, type into new machine).

**trust store**:
Each node keeps a local store of authorized peer pubkeys. TOFU after pairing: accept on first connect post-ceremony, warn on key change. Lives near `node.key`.

**fetch-code-from-any-node**:
Because every paired node holds the TOTP seed, the user can fetch the *current* pairing code from any already-paired machine — no phone required if a trusted node is handy. Via `spt pair --code` (prints the current code) or by asking a LiveAgent/ReadyAgent for it (the adapter supplies the agent instructions to surface it). Mild exposure note: anyone with CLI/agent access to a trusted node can read the current code — acceptable, since such access already implies a trusted position.

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

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

## Installation

spt-core is per-machine and harness-independent, so it installs *before* and *independent of* any adapter.

**Two install paths, (b) is the primitive:**
- **(a) harness-bootstrapped** — a harness plugin installs spt-core as its bootstrap step (today's cplugs model: `/plugin` pulls the plugin, which fetches the spt-core binary). The plugin's role shrinks to this bootstrap; spt-core self-updates thereafter.
- **(b) standalone** — the user installs spt-core directly with no harness, for a Pi node, a Shell-only node, or a headless server. Path (a) calls into (b).

**Installer form:** a one-line script (`curl | sh` / `irm | iex`) that fetches the **signed** binary for the platform, verifies it, places it, registers the binary dir on system-wide PATH (so adapters call `spt api …` cross-OS), and registers the daemon as an OS service. Chosen for the dev-tool audience, cross-platform reach, and because the binary self-updates after. **Marketplace-repackaging-friendly:** nodes are foreseen on novel platforms (Android, medium-power Linux handhelds), so the install must be easy to repackage for platform marketplaces (e.g. PortMaster for handhelds, F-Droid-style for Android) — a relocatable binary + minimal, non-OS-entangled install logic.

**Daemon lifecycle:** registered as a systemd user service (Linux) / Windows service or scheduled task for the always-on guarantee, with `spt api`-triggered auto-start as the fallback (above). The first network bind triggers the OS firewall prompt here.

**What "installed" comprises:** the `spt` binary + the broker + the `$SPT_HOME` root (holds `node.key`, trust store, registry, spools, in-memory-seed fallback state). **First run is idempotent + interactive-optional:** generates the node identity and starts the daemon unattended; pairing (and subnet naming) is a separate explicit step.

**Legacy migration at install:** standalone install **auto-detects an existing `claude_skill_owl` (modern SPT) install and offers migration** (the migration commitment above) — identity, agents, tracked Psyche context.

## Project infrastructure

**issue / feature tracking**:
GitHub for v1 (mature API + `gh` CLI already wired; de-risks any agent-files-issues automation). This is *project infrastructure*, not a runtime user dependency — it does not compromise the product's "no central operator" promise the way the removed runtime gh-sync did. **tangled.org** (git on AT Protocol, self-hostable knots) is documented as the principled migration target, aligned with spt-core's decentralized ethos — especially if agents ever file issues over spt's own P2P layer into a self-hosted tracker. v2+ story.

### Frontend (day-one)

The day-one headed frontend is a launcher/manager UX, not just a raw `attach`. The end user cares about *reaching a target agent*, not about PTY mechanics. The frontend must:

- **List** all running and historic endpoints.
- **Launch a historic endpoint** that isn't currently running, reusing the same manifest that originally launched it.
- **Tap into** (attach to) a running endpoint.
- **Init a new endpoint** using a known adapter (e.g. claude-code / spt-plugin).

This is the realization of the "guided resume" (resume by project or agent, XMB-style filtered selection) and "management GUI" (per-endpoint panes: latest session output, live/project context, psyche log) sketches. `attach` is one operation under this frontend, not the whole frontend.