<task-notification>
<task-id>a0ee45790ecb3e216</task-id>
<tool-use-id>toolu_01NKVCsw57jiQaNUe5cHkdLh</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\b494f5e6-5b4d-4145-aaaa-5585e1168377\tasks\a0ee45790ecb3e216.output</output-file>
<status>completed</status>
<summary>Agent "Research codex CLI log storage" completed</summary>
<result>I now have all the concrete details grounded in source. Here is my report.

---

# How OpenAI Codex CLI Stores Session Transcripts (vs. Claude Code)

Research grounded in the `openai/codex` Rust source (`codex-rs`), official OpenAI Codex docs, DeepWiki's code-indexed wiki, and GitHub discussions/issues. Where something is version-dependent or uncertain, I flag it.

## 1. Where session history lives on disk

**Path pattern (macOS/Linux):**
```
~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<conversation_id>.jsonl
```
- Files are called **rollout files**; the format is **JSONL** (newline-delimited JSON), one record per line.
- Directory layout is **hierarchical by date** — the recorder pushes `sessions/`, then year, month, day onto the path (`codex-rs/rollout/src/recorder.rs`).
- Filename format string is literally `format!("rollout-{date_str}-{conversation_id}.jsonl")`, where `date_str` comes from `[year]-[month]-[day]T[hour]-[minute]-[second]`. Example:
  `rollout-2025-05-07T17-24-21-5973b6c0-94b8-487b-a530-2aeb6098ae0e.jsonl`
- On Windows the root is the `%USERPROFILE%\.codex\` equivalent (the code uses the OS home dir + `.codex`), so `…\.codex\sessions\YYYY\MM\DD\`.
- Persistence can be **disabled** per run: `codex exec --ephemeral` runs "without persisting session rollout files to disk." So by default everything is persisted; ephemeral is opt-out.

**Important dual-store detail (recent versions):** Codex now maintains *two* layers — the JSONL rollout files **plus** a **SQLite "state DB"** that indexes rollout files for fast lookup/resumption (surfaced via the app-server's `thread/resume`). This is a meaningful divergence from Claude Code (see §5). It also creates a real failure mode: GitHub issue #21196 reports "state still has threads, files gone" — the DB can reference a rollout file that no longer exists on disk.

## 2. How a session is identified and addressed

- The identifier is a **`ThreadId`** (a UUID), historically/also called `conversation_id` and surfaced to users as `SESSION_ID`. The same UUID appears **both** in the filename **and** inside the file (in the `SessionMeta` record's `id` field).
- The id is **auto-generated at session start** and **cannot be overridden** via a flag. Per the maintainer in discussion #3827: you may rename files for your own reference, but resume relies on the **id stored inside the JSONL**, not the filename.
- **How an external tool locates a transcript for a given id:** Two viable strategies:
  1. **Glob the tree** for `**/rollout-*-<SESSION_ID>.jsonl` under `~/.codex/sessions/` (the id is embedded in the filename, so a filename match works without opening files). This is the simplest, dependency-free approach and is what community tools (e.g. `codex-trace`, `codex-history-list`) do.
  2. **Query the SQLite state DB** for the index (what the official app-server does), then open the referenced rollout file. More fragile for an external tool, and version-dependent.
- A user can obtain the id from the `codex resume` picker, from `/status` inside a session, or by inspecting the files.

## 3. Record / line schema

Each line is a **`RolloutLine`** wrapper = `{ timestamp, item }` where `item` is a **`RolloutItem`**, a serde-tagged enum:

```rust
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
pub enum RolloutItem {
    SessionMeta(SessionMetaLine),  // "session_meta"
    ResponseItem(ResponseItem),    // "response_item"
    Compacted(CompactedItem),      // "compacted"
    TurnContext(TurnContextItem),  // "turn_context"
    EventMsg(EventMsg),            // "event_msg"
}
```

So every line looks like: `{"timestamp":"…","type":"<variant>","payload":{…}}`.

**First line — `session_meta`** (`SessionMeta`/`SessionMetaLine`), carries session-level context used for resume:
`id` (ThreadId), `forked_from_id`, `timestamp`, `cwd`, `originator`, `cli_version`, `source`, `model_provider`, `base_instructions`, `agent_nickname`/`agent_role`, `dynamic_tools`, `memory_mode`, and a flattened optional `git` (`GitInfo`).

**Conversation content — `response_item`** (`ResponseItem`), serde `tag="type"`, snake_case variants. Key ones:
- `message` → `{ role: String, content: Vec<ContentItem>, phase }`. `ContentItem` is itself tagged: `input_text {text}`, `input_image {image_url, detail}`, `output_text {text}`. **Note: roles and content are nested inside a `content` array of typed parts** — input vs. output text are *different* variants, not a single `text` field.
- Tool calls/results: `function_call` (with arguments), `function_call_output` (tool result payload), `local_shell_call`, `custom_tool_call` / `custom_tool_call_output`, `web_search_call`, `image_generation_call`.
- `reasoning` (summary + optional content), `compaction`/`context_compaction`.

**Lifecycle/telemetry — `event_msg`** (`EventMsg`): `UserMessage`, `AgentMessage`, `TokenCount`, `TurnComplete`, plus (in extended mode) `ExecCommandEnd`, `Error`, `McpToolCallEnd`, etc.

**Persistence policy matters for parsers:** there are two modes. **Limited (default)** persists only essential markers (`UserMessage`, `AgentMessage`, `TokenCount`, `TurnComplete`); **Extended** adds diagnostic events. So the *same* logical message can appear as a structured `response_item` and/or as an `event_msg` depending on mode — a normalizer must decide which to treat as canonical to avoid duplicates.

## 4. Resuming a prior session

Yes. `codex resume`:
- `codex resume` → interactive picker of recent sessions (shows a summary; Enter reopens).
- `codex resume --last` → skip picker, continue the most recent session **in the current working directory**.
- `codex resume --last --all` → most recent session across **any** directory (resume is cwd-scoped by default).
- `codex resume <SESSION_ID>` → resume a specific session by UUID.

On resume, Codex loads the JSONL rollout (via the state DB index / `ThreadHistoryBuilder`), reconstructs messages + tool calls + context, and — importantly — **appends new turns to the existing rollout file** rather than creating a new one. The app-server's `thread/resume` **requires a materialized rollout file**; if it's missing, it errors (the #21196 data-loss case).

## 5. What differs from Claude Code's model — and complicates a generic parser

Claude Code: `~/.claude/projects/<project-hash>/<session-id>.jsonl`, one flat JSONL keyed directly by session id, project-partitioned by an encoded cwd. Codex differs in ways that matter for a "given a session id → normalized messages" seam:

1. **Locating the file is harder.** Codex partitions by **date** (`YYYY/MM/DD`), not by project/cwd, and the id is a *substring* of the filename, not the whole name. You can't construct the path from the id alone — you must **glob** the date tree (or query the SQLite DB). You also don't know the date, so the glob must be recursive. Claude Code lets you derive/guess the path far more directly.

2. **Two sources of truth.** Codex has a **SQLite state DB index alongside the JSONL files**, and they can desync (DB references a deleted rollout file — issue #21196). A robust seam should treat the JSONL as canonical and not depend on the DB, but be aware resume itself depends on the DB.

3. **Nested, doubly-tagged envelope.** Every record is `{timestamp, type, payload}` and the message payload is *itself* a tagged `ResponseItem` whose content is an array of tagged `ContentItem`s (`input_text` vs `output_text`). That's effectively **three levels** of `"type"` discrimination versus Claude Code's flatter per-line `{type, role, message…}`. Tool calls/results are separate top-level item types (`function_call`, `function_call_output`, `local_shell_call`, …), not embedded in a single assistant message.

4. **Mixed event vocabularies in one file.** `response_item` (model-API-shaped items) and `event_msg` (Codex lifecycle/telemetry) coexist, and the **persistence mode (Limited vs Extended)** changes which appear. A normalizer must dedupe overlapping representations and tolerate missing diagnostic events.

5. **Append-on-resume with forking.** Resumed sessions append to the original file (single id, growing transcript), but Codex also supports **forking** (`forked_from_id` in `SessionMeta`) — so reconstructing a full lineage may require following fork pointers across multiple files. Claude Code's model is closer to one-file-per-session.

6. **Filename ≠ identity.** Renaming a Codex rollout file does not change the session it resumes to; identity lives in `SessionMeta.id`. A parser keyed purely on filename can be fooled — read the first `session_meta` line to confirm the id.

7. **Ephemeral sessions may not exist on disk at all** (`codex exec --ephemeral`), and `codex exec` JSON streaming uses a *different* event vocabulary (`thread.started`, `turn.completed`, `item.*`) than the persisted rollout schema — don't conflate the live `--json` stream format with the on-disk rollout format.

**Net design implication for your seam:** for Codex, "given a session id → file" should be a recursive glob over `~/.codex/sessions/**/rollout-*-<id>.jsonl` (with a SQLite-DB fast path as optional), and the normalizer must flatten a 3-level tagged envelope, reconcile `response_item` vs `event_msg`, handle Limited/Extended mode gaps, and optionally follow `forked_from_id`. That's a notably richer adapter than the Claude Code path.

## Sources
- DeepWiki (code-indexed): [Rollout Persistence and Replay](https://deepwiki.com/openai/codex/3.5.2-rollout-persistence-and-replay), [Session Resumption](https://deepwiki.com/openai/codex/4.4-session-resumption)
- Source: [`codex-rs/protocol/src/protocol.rs`](https://github.com/openai/codex/blob/main/codex-rs/protocol/src/protocol.rs), [`codex-rs/protocol/src/models.rs`](https://github.com/openai/codex/blob/main/codex-rs/protocol/src/models.rs), [`codex-rs/rollout/src/recorder.rs`](https://github.com/openai/codex/blob/main/codex-rs/rollout/src/recorder.rs)
- Official docs: [Codex CLI command reference](https://developers.openai.com/codex/cli/reference), [Non-interactive mode (`--json`)](https://developers.openai.com/codex/noninteractive)
- GitHub: [Discussion #3827 (session/rollout files)](https://github.com/openai/codex/discussions/3827), [Discussion #1076 (resuming sessions)](https://github.com/openai/codex/discussions/1076), [Issue #21196 (DB vs missing rollout files)](https://github.com/openai/codex/issues/21196), [Issue #2288 (trajectory JSON for exec)](https://github.com/openai/codex/issues/2288)
- Community tools confirming the path/format: [PixelPaw-Labs/codex-trace](https://github.com/PixelPaw-Labs/codex-trace), [InventiveHQ: ~/.codex config locations](https://inventivehq.com/knowledge-base/openai/where-configuration-files-are-stored), [InventiveHQ: resume sessions](https://inventivehq.com/knowledge-base/openai/how-to-resume-sessions)

**Confidence notes:** Path pattern, filename format, JSONL `{timestamp,type,payload}` envelope, `SessionMeta`/`ResponseItem`/`ContentItem` field names, and the `codex resume` flags are directly grounded in current `main` source and official docs — high confidence. The **SQLite state-DB index** is a more recent addition tied to the app-server; its exact location/schema I did not pin down to a file path, so treat that layer as version-dependent and verify against the installed Codex version before depending on it.</result>
<usage><subagent_tokens>35253</subagent_tokens><tool_uses>11</tool_uses><duration_ms>130148</duration_ms></usage>
</task-notification>


1. >id optional--if not supplied, spt-core will have logic to function similar to no-id `/spt:live` runs today. so that means run a lone live agent if that's all the project has, show a picker and pro…


Design spt-core binary and messaging library architecture


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


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


Codex research back — strongly validates the dual-path history design. Folding the rationale in.


Edit

- The **echo-commune seam** is then just a command template (adapter picks the model) that consumes whichever history path is configured for the session.

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


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

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

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

+- **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.


Done. Codex findings, condensed: its transcript model is *much* harder than Claude Code's — date-partitioned, glob-to-locate (id is only a filename substring), 3-level tagged envelope, a SQLite index that can desync from the files, Limited/Extended modes, and session forking. That decisively confirms **adapter-owned normalize-command** (a built-in per-harness parser would be unbounded version-fragile maintenance), with **Path B native store** as the escape hatch. Folded into CONTEXT as the seam's rationale.

Still awaiting your call on the three open items from before the research landed:

1. **post-spawn boot nonce** — add a generation/boot nonce to the post-spawn payload so a respawn-after-crash can't be confused with a stale duplicate bind (guards hazard 2.4)? Recommend yes.
2. **UUID-injection vs post-spawn** — does UUID-injection *fully* suppress post-spawn (accepting that `parent_pid`/port/identity then can't be reported, and pid-indirection topologies lose proper session binding), or is post-spawn still fired for binding even when the UUID was injected (my recommendation)?
3. **Walk the inbound subcommand surface next?** — enumerate the commands-API adapters call (activity/idle, poll, post-spawn bind, hook-event reports: subagent start/stop, clear/compact, crash) one-by-one like we did outbound, then standalone install, then assemble the PRD.


-post-spawn hook: agree with all incl. suggestion. -agreed on post-spawn optionality and restrictions. adapter must commit to injecting session UUID + maintaining a stable PID to functionally forego post-spawn. -yes, subcommand surface next. note for clarity + distinction from commands *agents* call (like `$OWL ready`), commands-API commands should prefix with `api ` or `api-`.


-post-spawn hook: agree with all incl. suggestion. -agreed on post-spawn optionality and restrictions. adapter must commit to injecting session UUID + maintaining a stable PID to functionally forego p…


Locked. Capturing post-spawn finalization + the `api ` naming convention, then walking the inbound surface.


**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. Carries: 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, and optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below). Flips the perch from skeleton → live. (Skippable when spt-core injected a session UUID at spawn.)

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


**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-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. Carries: 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, and optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below). Flips the perch from skeleton → live. (Skippable when spt-core injected a session UUID at spawn.)

+**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:


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

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


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

+**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 update declaration** (manifest field):


Q15 — inbound `api` subcommand surface. The commands an adapter's hooks fire (machinery-facing, `api`-prefixed). Proposed set, each with: what fires it, what spt-core does. React/correct.

| `api` command | Adapter fires from | spt-core does |
|---|---|---|
| **`api bind`** | SessionStart-equiv (post-spawn) | session_id/parent_pid/identity/port/nonce → skeleton→live perch bind |
| **`api state <busy\|idle>`** | activity transitions (pre/post tool, prompt-idle) | writes activity/idle sentinel in session perch (delivery timing + presence) |
| **`api poll`** | hook that injects pending msgs (e.g. PreToolUse) | returns queued messages for the adapter to inject; drains non-deferred |
| **`api worker-start`** | SubagentStart-equiv | creates Worker perch under parent (nested, registry-tracked) |
| **`api worker-stop`** | SubagentStop-equiv | tears down Worker perch |
| **`api boundary <clear\|compact>`** | `/clear`, `/compact` equiv | fires split-echo-commune (catch-up brief to Self + Psyche), context resume |
| **`api session-end`** | Stop / SessionEnd-equiv | teardown / signoff / orphan handling trigger |
| **`api history-log`** | adapter's logging hook (Path B only) | ingests normalized records into spt-core-native history store |
| **`api presence`** | user-interaction signal | updates presence datum `(last_active_node, last_active_endpoint, ts)` |

Mapping note: these are the generalized forms of today's CC hooks (SessionStart, SubagentStart/Stop, PreToolUse, Stop, /clear). An adapter binds whichever its harness supports; a harness lacking a hook (e.g. no subagents) simply omits those.

Three questions:
1. **Merge `api state` and `api presence`?** Activity-state (busy/idle, per-session) and user-presence (which endpoint/node the user touched, per-subnet) overlap — a user interacting *is* an activity signal. Recommend **keep separate**: `state` is session-local delivery timing; `presence` is subnet-level user-location. Different consumers, different lifetimes. Agree?
2. **Is `api poll` inbound-contract or a delivery mechanism?** It's the "adapter hooks call poll commands" delivery path from seam #6 — arguably belongs to inject-input, not a distinct report. Recommend list it here anyway (it's an `api` command adapters call), cross-referenced to inject-input. OK?
3. **Missing any?** Notably: an `api alarm`-fire equivalent, or `api commune` (adapter-triggered commune)? Or are those agent-facing (`$OWL`-side), not `api`? My read: commune/alarm/signoff are *agent/user*-initiated (unprefixed surface), not harness-machinery — so they stay out of `api`. Confirm, or do any belong in `api`?


-bind: ok. could also be the sole perch-starting surface for the adapter-owned topology i.e. how modern spt works. or is that where `$LIVE start` is still used, given spt-plugin would still need the Monitor-bound poll listener? what do the startup flows look like for both topologies? -state: ok. i'm wondering if we should break out ops related to the `.more-done` sentinel (echo communes rely on it) into a dedicated command or extension of `api state`. it could live inside `api state idle` as the two coincide for modern spt IIRC. but other adapters may want more granular control over when echo communes fire/when the sentinel is written. thoughts? -poll: ok. maybe an optional flag to also drain deferred, for adapter flexibility. tangentially: some adapter-owned topologies may want a long-running listener poll to own as a child process. is that an api command, or..? -worker-start: ok. -worker-stop: ok. there also needs to be an `api worker-poll` for subagents to receive messages. -boundary: ok. likely needs to communicate the new session_id, given that changes after `/clear` or `/compact`. -session-end: ok. needs something like an `--erase` flag for agents whose perches and tracked history must be wiped. could be useful for i.e. a secondary adapter that functions like a more robust subagent/agent-spawned agent invocation surface. -history-log: ok. -presence: ok. presence may also be updated by way of broker detecting (not watching/logging) user input, in the spt-owned topology. --- an aside: every command invocation and/or supplied manifest needs to stick to + supply a unified `adapter_name` string per unique adapter, i.e. "claude-spt". you can probably guess the reason(s). --- your qs: 1. agree, separate. 2. ok. 3. correct, those are agent-initiated and stay out of api. if an adapter really wanted to, it could i.e. write the commune file itself. note that the commune and signoff commands are deprecated in modern spt in lieu of a file-drop flow. i previously noted how the manifest needs to identify where the poll listener should look for those files.


-bind: ok. could also be the sole perch-starting surface for the adapter-owned topology i.e. how modern spt works. or is that where `$LIVE start` is still used, given spt-plugin would still need the M…


Dense and good. The startup-flows question is the key one — let me design both, capture the locked refinements, then confirm.


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

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)

**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent):**
1. The session boots (user already in their CC/etc. session). The adapter's SessionStart hook fires **`api bind`** — identity + `parent_pid` + nonce, skeleton→live.
2. The session launches the long-running relay via its Monitor-equivalent: **`api listen <id>`** — streams the daemon's events into the session's stdout. (Heir to today's `$LIVE start`.)
3. The always-on daemon holds the perch, spool, registry, and runs the Psyche (headless, via the spawn-psyche seam) — no separate wrapper process. The relay is purely the delivery pipe.

**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).
3. The daemon injects events **directly into the PTY** — no relay. (During *activity*, delivery still defaults to hook-injection / the adapter's non-disruptive path, not raw PTY writes.)
4. Psyche is daemon-spawned, same as above.

So the old `$LIVE start` splits by topology: harness-hosted = `api bind` (auto, via hook) + `api listen` (relay, via Monitor); spt-hosted = daemon spawn-session + `api bind`. `api bind` is the common binding step in both, but it is *not* the sole perch-starting surface — harness-hosted additionally needs the `api listen` relay; spt-hosted additionally needs the daemon-side spawn.


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)

**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent):**
1. The session boots (user already in their CC/etc. session). The adapter's SessionStart hook fires **`api bind`** — identity + `parent_pid` + nonce, skeleton→live.
2. The session launches the long-running relay via its Monitor-equivalent: **`api listen <id>`** — streams the daemon's events into the session's stdout. (Heir to today's `$LIVE start`.)
3. The always-on daemon holds the perch, spool, registry, and runs the Psyche (headless, via the spawn-psyche seam) — no separate wrapper process. The relay is purely the delivery pipe.

**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).
3. The daemon injects events **directly into the PTY** — no relay. (During *activity*, delivery still defaults to hook-injection / the adapter's non-disruptive path, not raw PTY writes.)
4. Psyche is daemon-spawned, same as above.

So the old `$LIVE start` splits by topology: harness-hosted = `api bind` (auto, via hook) + `api listen` (relay, via Monitor); spt-hosted = daemon spawn-session + `api bind`. `api bind` is the common binding step in both, but it is *not* the sole perch-starting surface — harness-hosted additionally needs the `api listen` relay; spt-hosted additionally needs the daemon-side spawn.

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

+**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent):**

+1. The session boots (user already in their CC/etc. session). The adapter's SessionStart hook fires **`api bind`** — identity + `parent_pid` + nonce, skeleton→live.

+2. The session launches the long-running relay via its Monitor-equivalent: **`api listen <id>`** — streams the daemon's events into the session's stdout. (Heir to today's `$LIVE start`.)

+3. The always-on daemon holds the perch, spool, registry, and runs the Psyche (headless, via the spawn-psyche seam) — no separate wrapper process. The relay is purely the delivery pipe.

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

+3. The daemon injects events **directly into the PTY** — no relay. (During *activity*, delivery still defaults to hook-injection / the adapter's non-disruptive path, not raw PTY writes.)

+4. Psyche is daemon-spawned, same as above.

+So the old `$LIVE start` splits by topology: harness-hosted = `api bind` (auto, via hook) + `api listen` (relay, via Monitor); spt-hosted = daemon spawn-session + `api bind`. `api bind` is the common binding step in both, but it is *not* the sole perch-starting surface — harness-hosted additionally needs the `api listen` relay; spt-hosted additionally needs the daemon-side spawn.

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:


Captured. **Your question — startup flows, both topologies:**

**Harness-hosted (spt-plugin, CC is parent):** `$LIVE start` splits into two steps. (1) SessionStart hook fires `api bind` (identity + parent_pid + nonce, skeleton→live). (2) The session launches the long-running relay via Monitor: `api listen <id>` — streams daemon events to stdout (heir to today's `$LIVE start` poll loop). The daemon holds perch/spool/registry and **daemon-spawns the Psyche** (no separate wrapper). So `api bind` is *not* the sole start surface here — you still need `api listen` for the Monitor-bound relay, exactly as you suspected.

**spt-hosted (terminal wrapper / GUI):** (1) frontend → daemon runs spawn-session template into a broker PTY. (2) binary boots, fires `api bind` (or skips under the strict UUID+stable-pid commitment). (3) daemon injects via PTY directly, no relay. (4) Psyche daemon-spawned.

`api bind` = common binding step; harness-hosted adds `api listen`, spt-hosted adds daemon-side spawn.

**Locked refinements captured:** `api listen` (long-running relay, the answer to your poll-listener-as-child question), `api poll --include-deferred`, `api worker-poll`, `api boundary` carries new session_id, `api session-end --erase`, `api echo-gate` + `--no-gate` on `api state idle`, presence dual-source (adapter report + broker input-sensing), `adapter_name` on every invocation+manifest, commune/signoff stay file-drop (off both surfaces).

Two clarifications before standalone install:

1. **`api listen` prefix.** It's machinery-facing so `api` fits — but it's *long-running*, unlike the short-lived `api` callouts. Comfortable with a long-running command under the `api` namespace, or do you want it visually distinct (e.g. top-level `listen`)? Recommend keep `api listen` — owner is the adapter, semantics match.

2. **`adapter_name` reasons — confirming my read:** multi-adapter coexistence on one machine (daemon resolves each endpoint's manifest/seams by it), adapter-update ripple targeting, capability/manifest lookup key, telemetry. That the set? Or another reason driving it (e.g. cross-node — an instance of `ling` under `claude-spt` on desktop vs under `spt-codex` on laptop must not be treated as the same hostable type)?


that harness-hosted flow won't quite work. in that topology, note that the session doesn't start until the agent invokes `$LIVE start`. so it can't rely on SessionStart for the spawn seam(s). `$LIVE start` is also invoked behind the Monitor tool, so it becomes the long-running event relay. -so maybe SessionStart can create a sort of ephemeral "seed file" somewhere under `$SPT_HOME/`. the seed file is identified by PID, and contains the session details that would, under the spt-core-owned topology, be shared directly? then when the agent calls `$LIVE start <live_id>`, spt-core can create/revive the perch using the seed file, discovered via parent PID (which the `$LIVE start` command can self-discover, then share with spt-core). That's a rough idea. Thoughts? Anything missing? --- spt-hosted: -sounds mostly good. note that some adapters may choose to still use a relay over PTY injection for idle event delivery. needs to be configurable via manifest. -just a brainstorm: perhaps if we use the "session seed file" flow for harness-hosted, would it also make sense to use a "session catalyst file" for spt-hosted? so instead of firing `api bind`, i.e. SessionStart writes the catalyst file. idk if that really makes sense, or if it has any advantages. maybe that's harder on the system drive long-term, and should be avoided. thoughts? --- questions: 1. my points above should inform this. whatever the case, env vars should an agent to have a clean-looking invocation behind i.e. the Monitor tool. so something like `$SPT listen <live_id>` == `spt.exe api listen <live_id>`. 2. your first read is right. also, it just occurred to me that adapters should declare their minimum-supported `spt-core` version, and that declaration should live somewhere that can be read from before the entire adapter update takes place (to verify compatibility/expected supported features)


Seed-file model is right — and it exposes a clean principle. Let me rework the harness-hosted flow and capture.


**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent):**
1. The session boots (user already in their CC/etc. session). The adapter's SessionStart hook fires **`api bind`** — identity + `parent_pid` + nonce, skeleton→live.
2. The session launches the long-running relay via its Monitor-equivalent: **`api listen <id>`** — streams the daemon's events into the session's stdout. (Heir to today's `$LIVE start`.)
3. The always-on daemon holds the perch, spool, registry, and runs the Psyche (headless, via the spawn-psyche seam) — no separate wrapper process. The relay is purely the delivery pipe.

**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).
3. The daemon injects events **directly into the PTY** — no relay. (During *activity*, delivery still defaults to hook-injection / the adapter's non-disruptive path, not raw PTY writes.)
4. Psyche is daemon-spawned, same as above.

So the old `$LIVE start` splits by topology: harness-hosted = `api bind` (auto, via hook) + `api listen` (relay, via Monitor); spt-hosted = daemon spawn-session + `api bind`. `api bind` is the common binding step in both, but it is *not* the sole perch-starting surface — harness-hosted additionally needs the `api listen` relay; spt-hosted additionally needs the daemon-side spawn.

Governing principle: **a file bridge is used only when the daemon is NOT the launcher** (and therefore has no direct channel to the session). When the daemon *is* the launcher (spt-hosted), it already holds a channel and binds directly — no file.

**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. Instead, a **seed file** bridges the gap:
1. The harness's SessionStart hook writes an ephemeral **seed file** at `$SPT_HOME/seeds/<parent_pid>.json` (or equivalent), keyed by `parent_pid`, carrying the session details that the spt-hosted topology would share directly (`session_id`, `cwd`, `adapter_name`, harness specifics) — everything except the not-yet-chosen `live_id`.
2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. This command self-discovers its `parent_pid`, looks up the seed file by that pid, hands the seed to the daemon (which 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 hygiene:** seed keyed by `parent_pid` (stable across `/clear`, since the harness process persists) but validated against the caller's `session_id` to defeat PID-recycling (KNOWN-HAZARDS 5.1); rewritten in place on each SessionStart (so `session_id` stays current across `/clear`); stale seeds swept by mtime on daemon boot. If no seed exists (a harness with no SessionStart-equiv), `start` may carry the details directly as args/env — the seed is the preferred convenience, not the only path.

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


**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent):**
1. The session boots (user already in their CC/etc. session). The adapter's SessionStart hook fires **`api bind`** — identity + `parent_pid` + nonce, skeleton→live.
2. The session launches the long-running relay via its Monitor-equivalent: **`api listen <id>`** — streams the daemon's events into the session's stdout. (Heir to today's `$LIVE start`.)
3. The always-on daemon holds the perch, spool, registry, and runs the Psyche (headless, via the spawn-psyche seam) — no separate wrapper process. The relay is purely the delivery pipe.

**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).
3. The daemon injects events **directly into the PTY** — no relay. (During *activity*, delivery still defaults to hook-injection / the adapter's non-disruptive path, not raw PTY writes.)
4. Psyche is daemon-spawned, same as above.

So the old `$LIVE start` splits by topology: harness-hosted = `api bind` (auto, via hook) + `api listen` (relay, via Monitor); spt-hosted = daemon spawn-session + `api bind`. `api bind` is the common binding step in both, but it is *not* the sole perch-starting surface — harness-hosted additionally needs the `api listen` relay; spt-hosted additionally needs the daemon-side spawn.

Governing principle: **a file bridge is used only when the daemon is NOT the launcher** (and therefore has no direct channel to the session). When the daemon *is* the launcher (spt-hosted), it already holds a channel and binds directly — no file.

**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. Instead, a **seed file** bridges the gap:
1. The harness's SessionStart hook writes an ephemeral **seed file** at `$SPT_HOME/seeds/<parent_pid>.json` (or equivalent), keyed by `parent_pid`, carrying the session details that the spt-hosted topology would share directly (`session_id`, `cwd`, `adapter_name`, harness specifics) — everything except the not-yet-chosen `live_id`.
2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. This command self-discovers its `parent_pid`, looks up the seed file by that pid, hands the seed to the daemon (which 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 hygiene:** seed keyed by `parent_pid` (stable across `/clear`, since the harness process persists) but validated against the caller's `session_id` to defeat PID-recycling (KNOWN-HAZARDS 5.1); rewritten in place on each SessionStart (so `session_id` stays current across `/clear`); stale seeds swept by mtime on daemon boot. If no seed exists (a harness with no SessionStart-equiv), `start` may carry the details directly as args/env — the seed is the preferred convenience, not the only path.

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

-**Harness-hosted (e.g. spt-plugin; the harness binary is user-launched, harness is the parent):**

-1. The session boots (user already in their CC/etc. session). The adapter's SessionStart hook fires **`api bind`** — identity + `parent_pid` + nonce, skeleton→live.

-2. The session launches the long-running relay via its Monitor-equivalent: **`api listen <id>`** — streams the daemon's events into the session's stdout. (Heir to today's `$LIVE start`.)

-3. The always-on daemon holds the perch, spool, registry, and runs the Psyche (headless, via the spawn-psyche seam) — no separate wrapper process. The relay is purely the delivery pipe.

+Governing principle: **a file bridge is used only when the daemon is NOT the launcher** (and therefore has no direct channel to the session). When the daemon *is* the launcher (spt-hosted), it already holds a channel and binds directly — no file.

+**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. Instead, a **seed file** bridges the gap:

+1. The harness's SessionStart hook writes an ephemeral **seed file** at `$SPT_HOME/seeds/<parent_pid>.json` (or equivalent), keyed by `parent_pid`, carrying the session details that the spt-hosted topology would share directly (`session_id`, `cwd`, `adapter_name`, harness specifics) — everything except the not-yet-chosen `live_id`.

+2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. This command self-discovers its `parent_pid`, looks up the seed file by that pid, hands the seed to the daemon (which 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 hygiene:** seed keyed by `parent_pid` (stable across `/clear`, since the harness process persists) but validated against the caller's `session_id` to defeat PID-recycling (KNOWN-HAZARDS 5.1); rewritten in place on each SessionStart (so `session_id` stays current across `/clear`); stale seeds swept by mtime on daemon boot. If no seed exists (a harness with no SessionStart-equiv), `start` may carry the details directly as args/env — the seed is the preferred convenience, not the only path.

-2. The binary boots and fires **`api bind`** (or skips it under the strict UUID-injection + stable-pid commitment).

-3. The daemon injects events **directly into the PTY** — no relay. (During *activity*, delivery still defaults to hook-injection / the adapter's non-disruptive path, not raw PTY writes.)

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

-So the old `$LIVE start` splits by topology: harness-hosted = `api bind` (auto, via hook) + `api listen` (relay, via Monitor); spt-hosted = daemon spawn-session + `api bind`. `api bind` is the common binding step in both, but it is *not* the sole perch-starting surface — harness-hosted additionally needs the `api listen` relay; spt-hosted additionally needs the daemon-side spawn.

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

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:


that harness-hosted flow won't quite work. in that topology, note that the session doesn't start until the agent invokes `$LIVE start`. so it can't rely on SessionStart for the spawn seam(s). `$LIVE s…


Now the `min_spt_core_version` adapter compat gate + adapter_name reasons.


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

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


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

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


All captured. Confirmations:

- **Seed-file flow** for harness-hosted: SessionStart writes `$SPT_HOME/seeds/<parent_pid>.json` (session details minus the unchosen `live_id`); `$SPT listen <id>` self-discovers parent_pid, consumes the seed, binds, relays. Seed validated against `session_id` (defeats PID-recycling), rewritten per SessionStart (survives `/clear`), mtime-swept. Fallback: pass details directly when no SessionStart exists. This is the heir to skeleton-perch + session-resume-by-parent_pid.
- **No catalyst file for spt-hosted** — your drive-churn instinct is right. Principle: **file-bridge only when the daemon isn't the launcher.** spt-hosted, the daemon spawned the process and owns the PTY → direct `api bind`, no file. Asymmetry justified.
- **spt-hosted idle delivery** manifest-configurable (PTY / relay / HTTP per state) — captured.
- **`$SPT` = `spt api`** env alias for clean Monitor invocation; adapters inject aliases, core supplies subcommands.
- **`min_spt_core_version`** in the manifest header, readable *before* update applies → compat gate; distinct axis from wire-proto version. Captured.

Decision tree on the harness contract is now deep. Last topic before PRD —

**Q16 — Standalone installation.**

How does spt-core get onto a machine in the first place — *before* any adapter, since the plugin's role shrank to "bootstrap spt-core" and the daemon is per-machine always-on?

Sub-questions:

1. **Bootstrap trigger.** Two install paths exist: (a) a harness plugin installs spt-core as its bootstrap step (today's cplugs model — `/plugin` pulls the plugin, which fetches the spt-core binary), and (b) truly standalone — user installs spt-core directly with no harness (for a Pi node, a Shell-only node, or a headless server). **Recommend support both**, with (b) as the primitive and (a) calling into it. Agree?

2. **Standalone installer form.** For path (b): a one-line `curl | sh` / `irm | iex` script that fetches the signed binary for the platform, places it, and registers the daemon? Or a downloadable installer? **Recommend the script** (matches the dev-tool audience, cross-platform, and the binary self-updates after) — signed binary, verified post-download.

3. **Daemon lifecycle install.** The always-on daemon needs to start on boot + restart on crash. Register as a **systemd user service (Linux) / Windows service or scheduled task**? Or a lighter "launch-on-first-use + self-respawn" with no OS service registration? **Recommend OS-service registration** (systemd-user / Windows service) for the always-on guarantee, with first-use auto-start as the fallback. Tradeoff: install needs elevated-ish perms for service registration; the firewall prompt also lands here.

4. **What does "installed" comprise?** The `spt` binary + the broker + the `$SPT_HOME` root (node.key generated on first run, trust store, registry, spools, seeds dir). First-run = generate node identity, start daemon, (if joining an existing subnet) run pairing. **Recommend first-run is idempotent + interactive-optional** — generates identity + starts daemon unattended; pairing is a separate explicit step.

Confirm 1–4, or adjust? And: should standalone install try to **auto-detect an existing legacy `claude_skill_owl` install and offer migration** (the Q2 migration commitment) right at install time?


a quick point about the seed file flow: under this structure, adapters must worry about resolving `$SPT_HOME`. it's even more of an integration nuisance if the adapter supports multiple operating systems. so i *partially* walk it back with this: -spt-core installation should register its binary dir as a system-wide PATH env var, so adapters can simply call `spt api (whatever)` on any OS. -add something like `spt api seed --pid <PID>` (also takes whatever else needed for spawn), which creates the seed file... or just a memory entry within the running spt-core daemon? does this approach make sense to do? --- another thought: in cases where `$SPT listen` is invoked for the first spt session on the given machine, it should also be able to spin up the daemon if it isn't already running. right? --- 1. agree 2. agreed, script. note that i foresee needing to run nodes on more novel platforms like android devices and medium-power linux handhelds. the install method should be easy to adapt for various marketplaces. portmaster comes to mind. 3. OS-service registration agree. 4. agree. -auto-detect migration for claude_skill_owl: yes. --- before i forget: one feature of `spt-core` should be that the user can fetch their current pairing TOTP from any paired machine. so they can either call i.e. `spt pair --code`, or ask a LiveAgent/ReadyAgent for it (adapter would need to employ instructions for the agent). --- another thing: users should be prompted to name their subnet the first time two nodes pair. this way they can feel confident as they add more nodes and see the same subnet name every time.


Excellent refinements — `api seed` supersedes the seed *file* entirely (no `$SPT_HOME` for adapters). Reworking.


Governing principle: **a file bridge is used only when the daemon is NOT the launcher** (and therefore has no direct channel to the session). When the daemon *is* the launcher (spt-hosted), it already holds a channel and binds directly — no file.

**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. Instead, a **seed file** bridges the gap:
1. The harness's SessionStart hook writes an ephemeral **seed file** at `$SPT_HOME/seeds/<parent_pid>.json` (or equivalent), keyed by `parent_pid`, carrying the session details that the spt-hosted topology would share directly (`session_id`, `cwd`, `adapter_name`, harness specifics) — everything except the not-yet-chosen `live_id`.
2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. This command self-discovers its `parent_pid`, looks up the seed file by that pid, hands the seed to the daemon (which 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 hygiene:** seed keyed by `parent_pid` (stable across `/clear`, since the harness process persists) but validated against the caller's `session_id` to defeat PID-recycling (KNOWN-HAZARDS 5.1); rewritten in place on each SessionStart (so `session_id` stays current across `/clear`); stale seeds swept by mtime on daemon boot. If no seed exists (a harness with no SessionStart-equiv), `start` may carry the details directly as args/env — the seed is the preferred convenience, not the only path.

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


Governing principle: **a file bridge is used only when the daemon is NOT the launcher** (and therefore has no direct channel to the session). When the daemon *is* the launcher (spt-hosted), it already holds a channel and binds directly — no file.

**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. Instead, a **seed file** bridges the gap:
1. The harness's SessionStart hook writes an ephemeral **seed file** at `$SPT_HOME/seeds/<parent_pid>.json` (or equivalent), keyed by `parent_pid`, carrying the session details that the spt-hosted topology would share directly (`session_id`, `cwd`, `adapter_name`, harness specifics) — everything except the not-yet-chosen `live_id`.
2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. This command self-discovers its `parent_pid`, looks up the seed file by that pid, hands the seed to the daemon (which 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 hygiene:** seed keyed by `parent_pid` (stable across `/clear`, since the harness process persists) but validated against the caller's `session_id` to defeat PID-recycling (KNOWN-HAZARDS 5.1); rewritten in place on each SessionStart (so `session_id` stays current across `/clear`); stale seeds swept by mtime on daemon boot. If no seed exists (a harness with no SessionStart-equiv), `start` may carry the details directly as args/env — the seed is the preferred convenience, not the only path.

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

-Governing principle: **a file bridge is used only when the daemon is NOT the launcher** (and therefore has no direct channel to the session). When the daemon *is* the launcher (spt-hosted), it already holds a channel and binds directly — no file.

+**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. Instead, a **seed file** bridges the gap:

-1. The harness's SessionStart hook writes an ephemeral **seed file** at `$SPT_HOME/seeds/<parent_pid>.json` (or equivalent), keyed by `parent_pid`, carrying the session details that the spt-hosted topology would share directly (`session_id`, `cwd`, `adapter_name`, harness specifics) — everything except the not-yet-chosen `live_id`.

-2. The agent runs `/spt:live <id>` → the adapter's `$LIVE start <id>` alias = **`$SPT listen <id>`** (= `spt api listen <id>`), invoked via Monitor. This command self-discovers its `parent_pid`, looks up the seed file by that pid, hands the seed to the daemon (which creates/revives the perch binding `live_id` ↔ session details), then enters the long-running relay loop streaming events to stdout.

+**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.

-   - **Seed hygiene:** seed keyed by `parent_pid` (stable across `/clear`, since the harness process persists) but validated against the caller's `session_id` to defeat PID-recycling (KNOWN-HAZARDS 5.1); rewritten in place on each SessionStart (so `session_id` stays current across `/clear`); stale seeds swept by mtime on daemon boot. If no seed exists (a harness with no SessionStart-equiv), `start` may carry the details directly as args/env — the seed is the preferred convenience, not the only path.

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


a quick point about the seed file flow: under this structure, adapters must worry about resolving `$SPT_HOME`. it's even more of an integration nuisance if the adapter supports multiple operating syst…


**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`.

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


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

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

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


Now the standalone install section.


## 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