URL: http://localhost:5474/llms-full.txt
Content-Type: text/plain
Method: text

---

# spt-core — full docs export
# Generated: concatenation of every page of the node-local docs (http://localhost:5474) in reading order.

===== /index.md =====

# SPT developer docs

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

It lets coding agents running under different harnesses talk to each other —
across sessions, across projects, and across machines — with no central server.

> **Pick your path:**
>
> - **Developer** — you want agents on your machines messaging each other:
>   start with the [messaging quickstart](quickstart/messaging.md) (one
>   install line + three commands, under 10 minutes).
> - **Adapter developer / dev-agent** — you're integrating a harness or
>   building a shell against the public contract: start with the
>   [adapter quickstart](quickstart/adapter.md), then the
>   [harness contract](harness-contract/overview.md).

## Install

<!-- [doc->REQ-INSTALL-BOOTSTRAP-VERB] -->
Non-interactive, through the GitHub CLI (the release channel is private —
see [Installing](reference/install.md) for the full steps):

```sh
gh auth login       # once per machine, an account that can read the channel
gh release download --repo BigscreenVR/spt-bs-releases --pattern 'spt-x86_64-linux'
chmod +x ./spt-x86_64-linux && ./spt-x86_64-linux install
# Windows: --pattern 'spt-x86_64-windows.exe', then .\spt-x86_64-windows.exe install
```

Verify:

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

<!-- [doc->REQ-DOCS-3] Diátaxis four-mode separation + one-canonical-way, stated as the corpus's contract -->
## How these docs are organized

Each capability vertical carries the same four modes, never mixed: an
**overview** (why it exists + how it fits), a **tutorial** where one ships in
v0.1, **how-to guides**, and **reference**. There is one canonical way to do
each thing; deprecated or alternate paths are marked when they exist.

<!-- [doc->REQ-DOCS-1] the dual-audience contract surfaced to the second audience: agent exports, .md negotiation, schema, CLI-help-as-docs -->
## For AI agents reading this

These docs are served node-locally at `http://localhost:5474` (each release
ships its own version-matched copy; `spt docs url` prints the resolved URL).

- [`llms.txt`](http://localhost:5474/llms.txt) — curated
  index of these docs. [`llms-full.txt`](http://localhost:5474/llms-full.txt)
  — the full concatenated export.
- Append `.md` to any page URL for raw markdown (about 90% fewer tokens than
  the HTML).
- [`manifest.schema.json`](http://localhost:5474/manifest.schema.json)
  — the machine-readable adapter-manifest contract. Validate your manifest
  against it before registering.
- `spt <command> --help` is a first-class documentation surface; the
  [CLI reference](cli/reference.md) is generated from it and cannot drift.

===== /quickstart/messaging.md =====

# Quickstart: two agents exchange a message

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

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

Everything below uses real values and runs as written.

## 1. Install

<!-- [doc->REQ-INSTALL-BOOTSTRAP-VERB] -->
Download the platform binary with the GitHub CLI (once: `gh auth login` with
an account that can read the release channel), then let it install itself:

```sh
# Linux
gh release download --repo BigscreenVR/spt-bs-releases --pattern 'spt-x86_64-linux'
chmod +x ./spt-x86_64-linux
./spt-x86_64-linux install
```

```powershell
# Windows (PowerShell)
gh release download --repo BigscreenVR/spt-bs-releases --pattern 'spt-x86_64-windows.exe'
.\spt-x86_64-windows.exe install
```

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

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

## 2. Optional: link two machines

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

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

On your first machine, create the subnet. This reveals its joining secret, so
it needs elevation — just run it directly and `spt` requests elevation for you
(a `sudo` password prompt on Linux/macOS; run the terminal as Administrator on
Windows):

```sh
spt subnet create home
```

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

> Running non-interactively (no TTY to prompt on)? `spt` instead prints the
> exact elevated command to copy-paste — it uses the binary's absolute path so
> a user-local install (`~/.local/bin`) still resolves under `sudo`.

On the second machine, join it (this enrolls the machine, so it elevates the
same way):

```sh
spt subnet join home
```

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

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

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

</details>

## 3. Hand your receiver agent its prompt

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

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

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

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

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

## 4. Hand your sender agent its prompt

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

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

What the agent runs:

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

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

Sergey's session prints it immediately:

```text
<EVENT type="msg" from="lea">hello sergey - lea here</EVENT>
```

`SENT` means live delivery — sergey was listening. Each delivery is one
`<EVENT>` envelope line; the `from="lea"` attribute is the routing handle:
whoever receives this knows where a reply goes (`spt send lea`).
Bodies are HTML-escaped with newlines as `<br>`; oversized deliveries split
into `<EVENT-PART>` lines the receiver concatenates back. The exact entity
set and decode order live in
[the `<EVENT>` wire contract](../messaging/overview.md#the-event-wire-contract).

## 5. Deliver to someone who's offline

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

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

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

```console
$ spt ready sergey --once
READY:sergey
<EVENT type="msg" from="lea">ping while you were away</EVENT>
```

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

## 6. What just happened

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

## Next

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

===== /quickstart/adapter.md =====

# Quickstart: build an adapter

<!-- [doc->REQ-DOCS-2] the dev-agent killer quickstart: minimal adapter satisfying the manifest + api contract, walked via the shipped mock adapter -->
The "build a harness for spt-core" hello-world: take the reference
**mock adapter** apart, register it, drive the contract with real commands,
then swap in your own harness. No spt-core source required — the public
contract is the manifest plus the `spt api` surface.

> Integrating an agent harness and a building a driven surface (notifier,
> robot, sensor) are the same contract with a different manifest body. For
> the latter, read this page first, then
> [Shells: getting started](../shells/getting-started.md).

## 0. What an adapter is

A TOML **manifest** that declares what varies for your harness — how to spawn
a session, which of your hook events fire which `spt api` command, how spt-core
can read session history — plus whatever your harness already has (hooks,
plugin config). Command templates are **opaque strings**: spt-core fills
`{key}` placeholders and runs them. It never parses out a model, a tool list,
or a flag. Your harness's business stays yours.

## 1. Get the reference adapter

Every release ships the mock adapter's source. With spt-core
[installed](messaging.md#1-install) (gh is already authenticated from that
step):

```sh
gh release download --repo BigscreenVR/spt-bs-releases --pattern 'mock-adapter.zip'
unzip mock-adapter.zip -d mock-adapter
```

(Windows: same `gh release download`, then
`Expand-Archive mock-adapter.zip mock-adapter`.)

The interesting file is `mock-adapter/manifest.toml`. It is deliberately
harness-agnostic — generic event names, a trivial `mock-session` helper
standing in for a real harness binary.

## 2. Read the manifest

The header is the only mandatory section:

```toml
[adapter]
name = "mock"
kind = "harness"                  # or "shell" (a driven surface)
version = "1.0.0"
min_spt_core_version = "1.0.0"    # compat gate, readable before any install/update
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
```

Inbound: your harness's hook events, each firing one `spt api` command:

```toml
[hooks.SessionStart]
fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
reads = ["session_id", "parent_pid"]
can_inject = true     # this hook can surface text back into the agent's context

[hooks.Idle]
fires = "api state idle"
can_inject = false    # no inject channel -> spt-core uses its sentinel/relay fallback
```

`can_inject` is the load-bearing harness-varying fact: when a hook can't put
text in front of the agent, spt-core routes around it automatically.

Outbound: opaque session templates spt-core spawns with `{key}` placeholders
filled:

```toml
[session.self]
command = "mock-session --id {id} --session-id {session_id}"
detach = true
keys = ["id", "session_id"]
```

A real adapter's template is your harness's full command line — model, flags,
tools, everything — exactly as you'd type it.

The rest declares history access (`[history]`), env bridging (`[env.*]`),
input injection (`[inject]`), and session identity (`[identity]`). Every
section beyond `[adapter]` is optional; the
[manifest reference](../harness-contract/manifest.md) covers them all.

## 3. Validate and register

Two layers of validation, both mechanical:

- **Schema** — your manifest must validate against
  [`manifest.schema.json`](http://localhost:5474/manifest.schema.json).
  The schema is generated from the same code that parses manifests, so it is
  always current; closed vocabularies (adapter kinds, history strategies,
  update avenues, …) are enums in it.
- **Registration** — `spt adapter add` parses, validates (including
  cross-field rules the schema can't express), and registers in one step:

```console
$ spt adapter add ./mock-adapter
ADAPTER_ADD:mock:Harness:Copy (registered)
ADAPTER_INSTALL_SKIP: no [update] avenue (manifest-only adapter)
$ spt adapter list
mock: Harness Copy active (from ./mock-adapter)
```

A bad manifest is rejected here with a message naming the offending field —
nothing half-registers.

## 4. Drive the contract

Every machinery call your adapter makes carries `--adapter <name>` — that's
the rule that makes multi-harness nodes unambiguous. Ask spt-core what your
adapter declared:

```console
$ spt api --adapter mock --manifest ./mock-adapter/manifest.toml capability
LiveAgent
ReadyAgent
Worker
```

Now the harness-hosted startup flow, exactly what your `SessionStart` hook
will fire (here with a stand-in pid):

```console
$ spt api --adapter mock seed --pid 4242 --session-id demo-session-1
SEEDED:4242
```

`seed` records an ephemeral hand-off keyed by the parent process id; the
session's listener then consumes it with `spt api … listen` and holds the
perch. That seed→listen pair *is* harness-hosted startup. (The other
direction — spt-core spawning the session itself from your `[session.self]`
template, then `api bind` — is spt-hosted startup. Both are in the
[`spt api` reference](../harness-contract/api.md).)

## 5. Make it yours

1. Copy `manifest.toml`, set `name`, `version`, and your real
   `hostable_types`.
2. Point `[hooks.*]` at the events your harness actually fires, with honest
   `can_inject` values.
3. Replace each `[session.*].command` with your harness's real command line.
4. Pick the `[history]` strategy your harness permits (binary that emits
   history → `fetcher`; transcript file on disk → `locate_normalize`; you
   push via `api history-log` → `native`).
5. Validate against the schema, `spt adapter add` it, and fire the
   `capability`/`seed` calls above against your own manifest.

Building adapters against this contract is **unrestricted and royalty-free**
— see the [license split](https://github.com/BigscreenVR/spt-bs-releases#license)
(the release channel's README; `LICENSE-BINARY`'s adapter clause is the
operative text, shipped in the channel repo).

## Next

- **Checklist:** the [harness integration checklist](../harness-contract/integration-checklist.md)
  — every contract surface grouped by necessity, mapped to the interaction
  lifecycle, plus the beyond-the-API integrations that make an adapter feel
  native. Work it top to bottom when building a real harness.
- **Reference:** the complete [manifest reference](../harness-contract/manifest.md)
  and [`spt api` reference](../harness-contract/api.md).
- **How-to:** ship spt-core *with* your adapter — the
  [install-on-demand bootstrap pattern](../harness-contract/install-on-demand.md).
- **Concept:** where adapters sit in the [mental model](../concepts/overview.md).

===== /concepts/overview.md =====

# Mental model

What spt-core is, the five or six nouns everything else builds on, and how the
pieces fit. Read this once and the rest of the docs are mostly reference.

## The shape of the system

spt-core is **per-machine infrastructure for agents**. One binary (`spt`)
installs on each machine. It carries everything: the CLI, the messaging
substrate, the always-available daemon, and the networking layer. Agent
harnesses — Claude Code, Codex, Pi (the pi coding agent), anything — plug in
through a declarative **adapter manifest** and a small command surface
(`spt api …`). spt-core never contains harness-specific logic; adapters
declare what varies, spt-core does the work.

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

## Endpoints and perches

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

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

## Messaging

The primitive everything else uses. `spt send <id>` delivers live when the
target is listening, spools when it isn't; `spt ring <id>` is the blocking
ask (send + wait for the reply); reply routing on the structural `from`
makes answers cheap.
Payloads carry typed operations and file blobs, not just text. Try it: the
[messaging quickstart](../quickstart/messaging.md).

## The daemon: broker and brain

One **spt daemon** per machine owns all shared state: hosted session PTYs,
the network identity and endpoint, the registry, every spool, all lifecycle
loops. You never manage it — any `spt` invocation auto-starts it.

Internally it splits in two, and the split is what makes self-update seamless:

- the **broker** holds only what must never die: PTY masters, spawned child
  processes, listening sockets. It almost never updates.
- the **brain** holds all logic and restarts freely. An update swaps the
  brain while the broker keeps every session's process and byte stream
  intact — running agents don't notice.

## Live agents and the mind

A **live agent** is an agent endpoint with a persistent working memory. Its
context survives session resets and even machine moves through three
file-drop mechanisms (no special APIs inside the agent's session):

- **commune** — the agent drops a context delta; spt-core ingests it into the
  endpoint's tracked mind (two tiers: a *live* tier that follows the agent
  everywhere, and a *project* tier scoped to one project).
- **signoff** — a graceful goodbye: final commune, then teardown.
- **echo-commune** — when a session ends without a signoff, spt-core runs a
  bounded summarizer over the session's history so the context delta is
  captured anyway.

The mind syncs between paired machines, so reviving `sergey` elsewhere brings
his memory with him.

## Instances, dormancy, and rest

One endpoint can have **instances** on several nodes. Instances rest when
unused — **dormant** (warm, zero idle cost, instantly wakeable) or
**suspended** (cold) — and remain addressable while resting: messages for
them are held and delivered on wake. `spt endpoint wake sergey` re-activates
the seat in place; nothing is respawned.

## Subnets, pairing, and the network

Machines pair into **subnets** — private, named groups sharing a registry of
endpoints. Pairing is a one-time ceremony seeded by a TOTP code (the same
six digits an authenticator app shows); after that, connectivity is
zero-config peer-to-peer QUIC with relay fallback, no central server. Every
endpoint's visibility and sync scope is controlled per subnet; nothing is
shared by default with anyone you haven't paired with.

## The harness contract

The seam third parties build against — two halves:

- the **[manifest](../harness-contract/manifest.md)**: a TOML file declaring
  what varies per harness (how to spawn a session, which hooks fire, how to
  read history). Command templates are opaque strings; spt-core fills `{key}`
  placeholders and runs them. SPT is not a harness: models, flags, and tools
  are always the adapter's business.
- the **[`spt api` surface](../harness-contract/api.md)**: the inbound
  commands a harness's hooks fire to keep spt-core's state in sync (session
  started, went idle, session ended, …).

A working adapter is a manifest plus whatever the harness already has.
[Build one in the adapter quickstart](../quickstart/adapter.md).

## Self-update

Releases are signed (Ed25519, two-key trust anchor baked into every binary)
and propagate peer-to-peer: one machine fetches a release, its peers verify
and stage it from each other. Updates apply with the broker/brain split, so
no endpoint process terminates or suspends during a self-update — the
system's standing invariant.

## Where to go next

| You want to… | Go to |
|---|---|
| see two agents talk | [Messaging quickstart](../quickstart/messaging.md) |
| integrate a harness | [Adapter quickstart](../quickstart/adapter.md) → [Manifest reference](../harness-contract/manifest.md) |
| build a notifier/robot/sensor | [Shells](../shells/overview.md) |
| pair two machines | [Networking & subnets](../networking/overview.md) |
| every command and flag | [CLI reference](../cli/reference.md) |

===== /messaging/overview.md =====

# Messaging

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

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

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries its sender id structurally;
  the arriving `<EVENT from="…">` envelope surfaces it, and
  `spt send <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents.
- **Per-message send control (three orthogonal axes).** Each `spt send`
  carries one value per axis; every axis defaults to unrestricted:
  - **Delivery window** (*when*) — `--active-only` spools for the agent's own
    poll without waking a live listener (it reaches the agent at its next
    natural boundary instead of interrupting now; also held for resting
    dormant/suspended instances and released exactly once on wake). This
    **renames the older `--deferred`**, which still parses as a hidden alias.
    `--idle-only` holds until the target is idle, then delivers (the wake).
    Default delivers in whichever window fires first.
  - **Channel** (*through what*) — `--prefer-native` routes through the
    target's translation binary when one is running, else falls back to the
    standard delivery; `--force-native` uses the binary only (no fallback,
    no reroute — if no binary is live it reports non-delivery rather than
    spooling to another method). Default is unrestricted. The translation
    binary is the adapter's idle-delivery filter; an adapter declares it with
    a `[message-idle-translation-binary].command` (a program token plus args,
    new in v0.16.0 — the bare `path` form is deprecated) and spt-core
    lifecycle-manages it.
  - **Persistence** (*how long*) — `--ephemeral` drops the message if it
    can't be delivered in its accepted window instead of spooling; it is the
    one path allowed to drop silently (everything else spools and reports
    non-delivery). *In this release ephemeral evaporation applies to
    translation-binary delivery and TTL expiry; the harness-relay
    carrier-absence case is not yet wired.*
- **Opaque metadata.** `--json-payload '<json>'` attaches a JSON metadata
  block alongside the body. spt-core carries it verbatim across every rail
  and never interprets it — the **receiving adapter** parses it. It can't
  forge spt-core's own envelope attributes (it rides inside a single `json`
  value), and any sender may attach it.
- **Typed payloads.** Message bodies carry typed operations and file blobs,
  not just text — file transfers are addressable and progress-queryable
  mid-flight.

<!-- [doc->REQ-DOC-DELIVERY-VOCAB] the send-outcome vocabulary itself — the closed set of SENT/SENT(WAN)/QUEUED/QUEUED(idle-only)/DEFERRED/NO_PERCH + WAN failure tags with their exact conditions; the JSON-consumer view + digest/poll shapes live in reference/json-shapes.md -->

## Send outcomes — the closed set

Every `spt send` reports exactly one outcome line. The line goes to
**stderr** (stdout is reserved for message payloads); classify success by the
**exit code** — `0` for every delivered/spooled outcome, non-zero for every
failure. The token before the first `:` is stable; this set is complete as of
v0.26.0.

**Success (exit 0):**

| Line | Meaning |
|---|---|
| `SENT:<id>` | Delivered live to a listening target on this node. |
| `SENT(WAN):<id>@<node>` | Delivered cross-node, **receiver-confirmed** — printed only when the remote daemon acknowledged. A suffix annotates the confirmed disposition: ` (spooled)` — the remote daemon accepted it into the target's durable spool; ` (duplicate)` — the receiver had already processed this message (a safe dedup'd replay). No suffix = delivered live. |
| `QUEUED:<id>` | The perch exists but nothing is listening — spooled durably, drains on the target's next `ready`. Success, not an error. |
| `QUEUED(idle-only):<id>` | An `--idle-only` send holding for the target's idle window. |
| `DEFERRED:<id>` | An `--active-only` send spooled for the target's own next poll (never interrupts a live listener). |

**Failure (non-zero exit, stderr):**

| Line | Meaning |
|---|---|
| `NO_PERCH:<id> is not listening` | No perch for that id — identity is never invented on someone else's behalf. An `--active-only` send reports this with `(active-only stays local-only)`: the hook channel does not take the cross-node leg. |
| `WAN_NO_PERCH:<id> — no perch on <node>` | The route resolved to a node, but no perch lives there (a stale route — the endpoint may have moved or stopped). |
| `WAN_REFUSED:<id>@<node>` | The receiver denied the message (access gate). |
| `WAN_UNCONFIRMED:<id>@<node>` | No receiver acknowledgment — the peer may be offline or on an old version. The message may or may not have landed; only `SENT(WAN)` means confirmed. |
| `WAN_FAIL:<id> — <error>` | Cross-node transport failure. |
| `AMBIGUOUS:<id> — <why>` | Several nodes host that id — qualify (`<id>@<node>`). |
| `EMPTY_MSG` | Refused: empty body. |

## The `<EVENT>` wire contract

Every arriving message on an **agent** surface (`spt ready`, `api listen`,
`api poll`, `api worker-poll`) is one `<EVENT …>body</EVENT>` envelope —
never a bare body:

```text
<EVENT type="msg" from="lea">hello</EVENT>
<EVENT type="alarm" target-time="…" current-time="…">check the build</EVENT>
```

A body that is already a fully-formed typed envelope (`echo_commune`,
`notify`, `user-msg`, …) passes through **verbatim** — one envelope, never
re-wrapped, and the body's own `from` wins. On the listener stream an
oversized line splits into `<EVENT-PART seq="K/M" id="…">` chunks the
receiver reassembles; `api poll` / `api worker-poll` always emit one whole
envelope per message, never chunked.

**Escaping — the closed entity set.** Exactly four entities, plus the newline
token; there is no `&#39;`/`&apos;` (single quotes ride literal):

- **Encode (body):** `&` → `&amp;` **first**, then `<` → `&lt;`, `>` → `&gt;`,
  `"` → `&quot;`; then CRLF and lone CR normalize to LF, and LF → `<br>`.
- **Decode (body):** split/replace `<br>` → newline **first**, then
  `&lt;` → `<`, `&gt;` → `>`, `&quot;` → `"`, and `&amp;` → `&` **last**.
  Amp-last is the invariant that keeps an embedded `&amp;lt;` from
  double-decoding into `<`.
- **Attribute values:** the same four entities in the same order, with no
  `<br>` step (attribute values are line-safe by construction).
- Decode only the **extracted body substring** after parsing the envelope
  framing — never run the entity decode over the full line, or the framing
  tokens themselves unescape.

**MAC-stamped frames are a different surface.** The shell relay drain
(`api poll <shell-id> --link <token>`) emits raw stamped frames of the form
`<mac> <frame>` — a 64-hex-char HMAC-SHA256 over the frame bytes, one space,
then the frame — and is deliberately **not** `<EVENT>`-wrapped (the shell
child verifies the MAC and parses its own vocabulary). Agent-perch surfaces
never emit stamped frames; a parser of agent traffic only ever sees
`<EVENT>` / `<EVENT-PART>` lines.

## Addressing

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

## Commands

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

===== /lifecycle/overview.md =====

# Live-agent lifecycle

What makes an agent endpoint a *persistent being* rather than a disposable
session: identity that survives resets, a working memory that follows it
across machines, and graceful endings that never lose context.

## The pieces

- **Perch** — the durable seat (identity, spool, state). Sessions attach to
  it (`api bind`/`listen`), reset across it (`api boundary`), and end
  without destroying it (`api session-end`).
- **Bringup states** — `spt endpoint run` creates the perch, starts the
  harness session, and the harness calls `api bind` to bring it **online**.
  Between the session starting and that bind the endpoint is **unbound**: a
  live, attachable session — `spt rc <id>` connects to it to watch or clear a
  bringup prompt *before* bind — that is not yet message-addressable (a `send`
  waits for online). The picker and `spt endpoint list` show it as a
  distinct **unbound** row — see the status-square legend below — not a true
  offline one. *(since v0.14.0)*
- **The mind, in two tiers** — a *live* tier (who the agent is, what it's
  doing) that follows the endpoint everywhere, and a *project* tier scoped
  to one project. Both are versioned, tracked storage, synced to paired
  machines with the same scoping.
- **Commune** — the agent drops `<id>-commune.md` into the adapter's watched
  directory; spt-core ingests the delta into the right tier. A file-drop,
  not a command — any harness that can write a file can commune.
- **Signoff** — the graceful ending: final commune, then teardown
  (`spt endpoint shutdown` / `api shutdown`). The echo-commune fires
  **before** teardown, always.
- **Echo-commune** — sessions that end *without* a signoff keep their
  delta: spt-core runs the adapter's bounded summarizer template over the
  session history and ingests the result. The **echo gate** sentinel
  (armed on idle, cleared by graceful signoff) is what marks the need.
- **Psyche** — the endpoint's persistent-context companion, driven as a
  **bounded per-event turn** (not a resident process): the daemon runs one
  `[session.psyche_resume]` turn per event (`[session.psyche_init]` is the
  go-live gate, never spawned).

## Rest and wake

Endpoints rest instead of dying: **dormant** (warm — zero idle compute,
instantly wakeable) or **suspended** (cold), explicitly via
`spt endpoint suspend` or on attention-shift. Resting instances stay
addressable; deferred messages are held and released exactly once on wake
(`spt endpoint wake`). Every active→resting edge fires a **transition echo**
so the final context delta lands before the lights go out.

## Reading the picker

The `spt endpoint run` picker (and `spt endpoint list`) marks each endpoint
with a status square. **A filled square means you can act now** — control it
if it is online, or wake it if it is suspended; **a hollow square means you
cannot** (no control seat, or the machine is gone). Endpoints on other
machines in the subnet now render with the **same** square as local ones, so
a remote row tells you everything a local row does. *(remote parity since
v0.17.0)*

| Square | State | What it means |
|---|---|---|
| green ■ | online · free | bound and message-addressable; you can control it |
| blue ■ | online · controlled | someone is driving it (the detail pane names the controlling node) |
| red ■ | online · unbound | a live session not yet message-bound — attachable with `spt rc`, needs attention |
| amber ▢ | online · harness-only | visible but has no control seat, so it cannot be controlled |
| gray ■ | suspended | cold but its machine is up — wakeable |
| gray ▢ | offline | the machine is down (only ever seen for remote endpoints) |

## Commands

`spt endpoint shutdown` · `endpoint suspend` · `endpoint wake` · the `api`
lifecycle calls ([reference](../harness-contract/api.md#session-lifecycle)).

Agents bringing themselves up live read `spt how-to live` — the in-binary,
always-current bringup guidance (the persistent listen relay, the Psyche seam,
ready-vs-live).

*Deeper tutorial coming with the docs' next tier; the contract above is
complete and current.*

===== /terminal/overview.md =====

# Terminal hosting

spt-core can *own* agent sessions in its own terminal layer: the daemon's
broker holds a real PTY per hosted session, which is what makes sessions
supervisable, attachable from other machines, and immune to self-update.

## What the broker holding the PTY buys

- **spt-hosted startup** — spt-core spawns sessions itself from the
  manifest's `[session.self]` template and binds them (`api bind`), instead
  of waiting inside someone else's process tree.
- **Remote attach** — a byte-stream viewport onto a live session from any
  paired node (compute and files stay on the hosting node). Restart-safe:
  reconnects resume the stream without gaps or duplicates.
- **Input injection** — `send-keys`/`send-line` style injection per the
  adapter's declared `[inject]` methods, respecting activity state (never
  disrupt a working agent).
- **The live digest** — `spt endpoint digest <id>` shows an at-a-glance view of
  what a session is doing now (`--follow` streams changes), **projected from the
  endpoint's normalized session logs** (the digest-record contract over
  `[history]`), never the PTY byte stream. Topology-independent — it works for a
  harness-hosted endpoint with no broker PTY. For scripted, turn-end consumption,
  `--json` adds an incremental cursor (v0.16.0): `--last <N>` reads the last N
  turns; closed-turn transcript entries (`Agent`/`ToolSprint`) carry a stable
  `seq` and each turn its `input_seq` — `Boundary`/`Context` entries never carry
  one, and a `partial` trailing turn's entries carry none until it closes;
  `--after <seq>` cursors over `seq`/`input_seq` and returns only what is
  newer. See the
  [integration checklist](../harness-contract/integration-checklist.md#incremental-digest-consumption--the---json-cursor).
- **Update immunity** — PTYs live in the broker, logic in the brain; a
  self-update swaps the brain while every hosted process and byte stream
  stays intact.

Activity and idleness are always **reported** (`api state busy|idle`), never
inferred from terminal quiescence — quiet terminals lie.

## Commands

`spt endpoint digest` · the attach surface · [`spt api` injection-adjacent calls](../harness-contract/api.md).

*Deeper tutorial coming with the docs' next tier.*

===== /networking/overview.md =====

# Networking & subnets

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

## The model

- **Node identity** — each machine holds an Ed25519 keypair; the public key
  *is* its network identity. Connections are mutually authenticated QUIC,
  end-to-end encrypted, peer-to-peer with NAT hole-punching and public-relay
  fallback (you can self-host the relay, or disable it for LAN/air-gapped
  use — the default relays carry only encrypted traffic they cannot read).
  Nodes also carry a human **label** (the hostname by default): views render
  `HFENDULEAM (bcead52b…)`, and `@node` qualifiers accept the label or a
  key prefix — several machines sharing a label are never guessed between.
- **Subnets** — machines join into named groups. A subnet shares: the
  endpoint registry (who exists, where, what state), context sync for its
  endpoints, notifications, and staged self-updates. Nothing is shared with
  nodes outside the subnet, ever.
- **Joining** — a one-time, code-authenticated ceremony. On a member
  machine, `spt subnet show-code` prints the current six digits (and an
  `otpauth://` URI — put the seed in your authenticator app); on the new
  machine, `spt subnet join <name>` finds a member over LAN + relay, then
  prompts for the code and runs the exchange. Finding a member happens
  *before* you enter the code, so the code you type is always fresh at the
  moment of pairing — a slow search never causes a just-read code to be
  rejected, and re-entering a code after a typo retries the pairing only
  (it does not restart the search). The search shows elapsed time while it
  runs, and on failure reports why (add `--verbose` for a full diagnostic
  dump); `--code <digits>` skips the prompt for non-interactive use. The
  code bootstraps a PAKE key exchange — the code is never the key, and a
  wrong guess learns nothing. Both sides pin each other's node keys on
  success (trust-on-first-use; key changes warn and never auto-apply).
  Every member machine answers join attempts automatically — no arming step
  on the existing fleet. *(two-phase join since v0.17.0)*
- **Elevation gates** — `subnet create` (reveals a fresh subnet's joining
  secret) and `subnet join` (enrolls the whole machine) require an elevated
  terminal; `subnet status` is read-only and ungated, and never prints
  secrets.
- **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be
  hidden from a subnet (neither advertised nor routable) and its mind syncs
  only to subnets on its membership list. Both default conservative;
  unconfigured means *not shared*.
- **Home subnet** — an endpoint is *homed* to exactly one subnet when it is
  created, and that home is permanent (it sets where the endpoint's identity
  lives and its default sync scope). On a node in a single subnet the home is
  chosen automatically; on a node in **more than one** subnet, `spt endpoint
  run` requires `--subnet <name>` — interactively it proposes a
  most-recently-used default and asks you to confirm, and non-interactively it
  refuses with the subnet list rather than guessing. *(since v0.14.0)*
- **Resource registry** — endpoints may advertise a free-text service blurb
  (`spt endpoint description set` to author; `spt endpoint list --detail`
  to browse) — an agent yellow-pages over visible rows only.

## The walkthrough

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

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

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

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

## Troubleshooting a join

A join searches for a member over every IP family your machine can actually
reach. At startup the daemon probes IPv4 and IPv6 once and uses only the
families that work — so a network that resolves IPv6 addresses but cannot
reach them (a common half-broken setup) no longer silently consumes the whole
search window. *(since v0.17.0)*

- **See what happened.** `spt subnet join <name> --verbose` prints, on
  failure, which IP families were usable, the time window it searched, how
  many attempts it made, and the last concrete error — enough to tell a dead
  subnet from a wrong code from a network problem.
- **Force an IP family off.** Set `SPT_DISABLE_IPV6=1` (or
  `SPT_DISABLE_IPV4=1`) to make the daemon skip that family regardless of the
  probe — a deterministic override for a misbehaving network. Setting both is
  an error. The probe is automatic; reach for these only to pin behaviour.
- **Quick discriminator.** If a join hangs only over the wider internet,
  check whether IPv6 reaches the relay: a working IPv4 path with a dead IPv6
  one is the classic case the per-family probe handles for you.

## What rides it

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

## Commands

`spt subnet` (`status` · `create` · `join` · `show-code` · `notify` ·
`attach`/`detach` · `leave` · `prune`) · `spt endpoint list --detail` ·
`spt endpoint description` · the qualified addressing forms
(`[subnet:]id[@node]`, where `@node` is a label or key prefix) —
[CLI reference](../cli/reference.md).

===== /harness-contract/overview.md =====

# Harness contract

The seam everything third-party builds against. spt-core contains zero
harness-specific logic; a harness (or a driven surface) interfaces through
exactly two things:

1. **The [runtime manifest](manifest.md)** — a declarative TOML file stating
   what varies for this harness: how to spawn sessions, which hook events
   fire which commands, how history is read, how the adapter updates.
   Command templates are opaque strings; spt-core fills `{key}` placeholders
   and runs them.
2. **The [`spt api` surface](api.md)** — the imperative entry points the
   harness's hooks fire to keep spt-core's state honest: session started,
   went idle, hit a context boundary, ended.

That's the whole integration surface. An adapter is a manifest plus the
harness's own native extension points — there is no SDK to link, no daemon to
embed, no protocol to speak beyond running `spt`.

```text
  your harness                         spt-core
 ┌────────────────────┐             ┌──────────────────────────┐
 │ hooks ─────────────┼── api … ───►│ perches · spools ·       │
 │ (SessionStart,     │             │ lifecycle · registry     │
 │  Idle, End, …)     │             │                          │
 │                    │◄────────────┼─ spawns [session.*]      │
 │ sessions           │  templates  │  templates, keys filled  │
 └────────────────────┘             └──────────────────────────┘
          ▲                                      ▲
          └────────── manifest.toml declares both seams
```

## Where to go

- **Start:** [the adapter quickstart](../quickstart/adapter.md) — take the
  reference mock adapter apart and drive the contract in minutes.
- **Build it all:** the [integration checklist](integration-checklist.md) —
  every surface by necessity, mapped to the interaction lifecycle.
- **Reference:** [manifest](manifest.md) · [`spt api`](api.md) ·
  [`manifest.schema.json`](http://localhost:5474/manifest.schema.json).
- **Ship it:** [install-on-demand bootstrap](install-on-demand.md) — how an
  adapter brings spt-core with it.
- **Driven surfaces:** [Shells](../shells/overview.md) — the `kind = "shell"`
  flavor of the same contract.

Building adapters, shells, or integrations against this contract is
**unrestricted and royalty-free** — see the
[license split](https://github.com/SaberMage/spt-releases#license).

===== /harness-contract/integration-checklist.md =====

# Harness integration checklist

<!-- [doc->REQ-DOCS-2] the harness-author checklist: every contract surface a harness touches, grouped by necessity, mapped to the interaction lifecycle, with the modern Claude Code adapter (spt-claude-code) as the worked example -->

A working list for building a harness against spt-core. The
[adapter quickstart](../quickstart/adapter.md) gets one adapter breathing in
ten minutes; this page is the *complete* surface — every manifest section and
`spt api` command a harness touches, **grouped by how badly you need it**, each
tagged with the **feature it buys** and **where in the interaction lifecycle**
it fires.

Two seams only (the [contract overview](overview.md)): the
[manifest](manifest.md) (declarative TOML) and the [`spt api` surface](api.md)
(imperative entry points your hooks fire). Nothing here is an SDK call —
everything is a manifest field or an `spt` invocation.

> **The running example is [spt-claude-code](https://github.com/SaberMage/spt-releases)** —
> the modern Claude Code harness rebuilt on spt-core (the v1 reference adapter).
> Where a row says *"claude-code: …"* that is how that harness wires the
> surface. Concrete commands below are real and shippable today; the shipped
> harness-agnostic exercise is the [mock adapter](../quickstart/adapter.md).

## The interaction lifecycle

Every surface below belongs to one stage of a harness's life with spt-core:

```text
 REGISTER ─► START ─► RUN ─────────────► BOUNDARY ─► END ─► KEEP-CURRENT
 adapter    perch    messaging /          context     tear   self-update
 add        seed→    activity /           clear /     down    + ripple
            listen   history / inject     compact
```

---

## Group 1 — Required (no adapter exists without these)

The contract floor. Miss one and spt-core cannot host your sessions.

| Surface | Feature it buys | Lifecycle stage |
| --- | --- | --- |
| **`[adapter]` manifest header** (`name`, `kind`, `version`, `min_spt_core_version`, `hostable_types`) | Identity + the compat gate spt-core reads *before* any install/update; declares which endpoint types you can host | REGISTER |
| **`spt adapter add <dir>`** | Parses + schema-validates + records the manifest; a bad field is rejected here, nothing half-registers | REGISTER |
| **`[adapter] host_binaries`** (harness-hosted) | The bind-time match-key — names the harness exe(s) you host, so `seed`/`listen` resolve your adapter with **no `--adapter`** (since v0.9.0). `--adapter <name[:profile]>` stays available as an optional override | REGISTER |
| **Startup pair — pick one flow:**<br>• harness-hosted: `[hooks.SessionStart] → api seed --pid {parent_pid} --session-id {session_id}` then the session's `api listen <id>`<br>• spt-hosted: `[session.self]` template (spt-core spawns it) then `api bind <id> --set-session-id <sid>` | A registered, held perch — the thing messages and lifecycle attach to. `seed→listen` = you own the process; `spawn→bind` = spt-core owns it | START |
| **`api session-end <id>`** (or `api shutdown`, below) | Clean teardown that PRESERVES the spool + history so the next `listen`/`poll` drains the backlog | END |

**claude-code:** `SessionStart` hook fires `api seed`; the Claude Code session
runs `api listen` as its blocking listener (harness-hosted). `SessionEnd` fires
`api session-end` (soft — context survives a `/clear` and a relaunch).

---

## Group 2 — Recommended (the integration is hollow without them)

Skippable to *boot*, but the harness feels broken without them — no inbound
messages, identity lost on a context reset, no activity signal.

| Surface | Feature it buys | Lifecycle stage |
| --- | --- | --- |
| **`[hooks.Idle] → api state idle`** (and `api state busy`) | Honest activity — spt-core never infers idleness from terminal quiescence (it lies). Arms the echo gate, drives Psyche pulses + most-recently-active routing | RUN |
| **`[inject]` channels** (`activity` / `idle`) + **`api poll <id> --include-deferred`** | Inbound message delivery. Declares HOW spt-core reaches the agent (hook inject vs. pull-relay); `poll` is the pull path for hooks that can't inject | RUN |
| **Honest `can_inject` per hook** | Lets spt-core route around a hook that can't surface text — the load-bearing harness-varying fact | RUN |
| **`api boundary <clear\|compact> <id> --to-session-id <new> --session-id <prior>`** | The endpoint's identity, spool, and history survive a context reset under a new session id. **The proof is the PRIOR sid** (`--to-session-id` is payload, not proof): persist the current sid at every SessionStart in adapter-owned state keyed by endpoint id, present it here; resolve the id from `$SPT_ENDPOINT_ID`; surface any refusal LOUDLY — a silent skip/refusal strands the perch on the dead sid (no delivery until relaunch). **Validate end-to-end: after a reset, assert the perch record's session id actually ROTATED and a post-reset message DELIVERS** — a session that looks healthy can hide a stranded perch (see `api boundary` in api.md) | BOUNDARY |
| **`api psyche-download <id>`** (fire from SessionStart, inject its stdout) | The agent resumes **with its mind** — pulls the durable two-tier context (role / live / project) **plus** any not-yet-synthesized commune as `<pending-*>` slices, for the hook to inject as additional context. `api boundary` makes the mind *survive* a reset; this is how the next session reads it **back in**. Without it, a resumed session starts blank of its accumulated context | BOUNDARY / START |
| **`[history]` strategy** (`fetcher` / `locate_normalize` / `native` + `api history-log`) | spt-core can read the session transcript — feeds the live digest and mind sync | RUN |
| **`[identity]`** (`session_id_source`, `parent_ancestor_name`) | Post-spawn id resolution when the harness mints the session id itself | START |
| **`[env.*]` bridge** (e.g. `OWL_SESSION_ID`) | The session learns its own endpoint id / context the harness must inject | START |
| **`[update]` avenue + command** | Ripple-update: spt-core refreshes your adapter alongside its own self-update (REQ-UPD-5); also the install-on-demand bootstrap | KEEP-CURRENT |
| **`[update.post]` post-step** (since v0.16.0) | A delegated step that runs **after** the primary avenue resolves — under `spt adapter update` **and** `spt adapter add` (install is the first update; since v0.19.0) — pull the `.spt` **and** run an in-harness sync from one lever. Foreground + bounded (120 s, never backgrounded); runs unconditionally; reads a published JSON line on stdin (`adapter_applied`, `version`, `previous_version`, `adapter_dir`, …); its stdout decides the post-update notice (custom text supersedes `[update].message`, the sentinel `!!update-message!!` fires it, empty is silent); failure is loud (`ADAPTER_UPDATE_POST_FAIL` on stderr + nonzero CLI exit) and isolated (never rolls back the pull — and the static message still fires, so verify-then-notify: see the manifest reference) | KEEP-CURRENT |

**claude-code:** `Idle` hook → `api state idle`; messages arrive over the hook
inject channel (`can_inject = true`), pull-relay fallback when busy.
`PreCompact`/clear hooks → `api boundary`; the `SessionStart` hook also runs
`api psyche-download <id>` and injects stdout, so a session resumes with its
accumulated two-tier mind (plus any pending commune). `[history] strategy =
"fetcher"` (Claude Code's transcript is a binary the fetcher reads). `[update]
avenue = "delegated"`, `command = "claude plugin update spt"` — the harness's
own updater is the avenue.

---

## Group 3 — Optional (capability-specific)

Reach for these when the capability applies; ignore them otherwise.

| Surface | Feature it buys | Lifecycle stage |
| --- | --- | --- |
| **`api shutdown <id>`** | Graceful signoff — runs the final echo-commune BEFORE teardown so the context delta is never lost to ordering | END |
| **`api presence <id>` / `api driven-by <id>`** | Most-recently-active resolution across the subnet; lets a session tell local input from remote-drive | RUN |
| **Workers** (`api worker-start <parent>` — the worker id is core-minted `<parent>-w<N>`, read it from stdout; `worker-poll <id>`/`worker-stop <id>` auth by the parent's session id, no token — breaking change in v0.27.0) | Nested, short-lived sub-agents under a parent endpoint | RUN |
| **`[digest]` extractor** (or `api digest-entry`) | A live activity digest (`spt endpoint digest`) — declare an extractor mapping your native log → the `{role, text, tool, ts}` contract (ADR-0019; its OWN seam, no longer riding `[history]`). Spans `/clear` via the session ledger; validate with `spt adapter digest-proof`. **Classify a delivered user-facing message as a turn-opening `input` record** (see below) so the v0.16.0 `--last`/`seq` cursor keeps its granularity | RUN |
| **`[session.notif]` template** | Native OS notification render (toast / shell alert) for consent + capability prompts, instead of burying them in agent output | RUN |
| **`[session.resume]` template** (spt-hosted) | The **native-resume** sibling of `[session.self]`: spt-core picks it over `[session.self]` when a bringup carries a prior session (`spt endpoint run --resume`, or the picker's *Resume from history*). Declare your harness's native-resume verb (e.g. `claude -r {session_id}`) — **skip it and a resume re-runs the fresh command → a blank transcript.** spt-core lands the PTY in the session's recorded project cwd (a harness resolves a transcript by `session_id` + cwd) | START |
| **`[message-idle-translation-binary]`** (spt-hosted) | A lifecycle-managed `stdin→stdout` JSON-lines binary that turns inbound `<EVENT>` messages into keystroke-commands spt-core applies to the PTY **atomically** (coexists with a live `spt rc` controller). The agnostic way to deliver messages into an **idle** spt-hosted session; busy delivery stays your `[inject]` hook path. Declare it with a `command` (program + args; adapter-static `{adapter_dir}`/`{adapter_name}` subst only — **no** session keys; new in v0.16.0, the bare `path` is deprecated). Validate the emit contract with `spt adapter translate-proof` | RUN |
| **`[adapter] shortcut_basename`** | Names the picker-generated project-root launcher `<basename>-<id>` (the `spt endpoint run` `s` keybind) — your harness's brand instead of the `spt-<id>` default | START |
| **Shell surfaces** (`kind = "shell"`: `api bind-shell --link`, `api emit`, `api owner-shutdown`, the `[shell]` body) | Driven surfaces — notifiers, sensors, power buttons — authenticated by the launch link token alone. See [Shells](../shells/overview.md) | START / RUN |

**claude-code:** uses `api shutdown` for graceful `/signoff`; declares a
`[digest]` extractor mapping its per-session JSONL → the digest-record contract so
`spt endpoint digest` shows live tool calls and spans `/clear`; declares
`shortcut_basename = "cc"` so the picker's generated launcher is `cc-<id>` (vs the
`spt-<id>` default); declares `[session.resume]` as `claude -r {session_id} …` so a
picker *Resume from history* reloads the real transcript (not a blank session);
declares a `[message-idle-translation-binary]` (`cc-spt-idle-translate`) so inbound
messages reach an idle session as proper keystrokes; no shell body (it is a harness,
not a driven surface).

---

## Group 4 — Beyond the API: integrations that make it good

Not contract surfaces — no `api` command, no required field — but the
difference between an adapter that *works* and one that feels native. **Strongly
recommended.**

| Integration | What it is | Why it matters |
| --- | --- | --- |
| **Commune / signoff file-drops** | The agent writes `<endpoint_id>-commune.md` (delta context) or `<endpoint_id>-signoff.md` (final save) into the manifest's watched `commune_dir` / `signoff_dir`; spt-core's watcher ingests it. **Delivered as a file-drop by design.** | The two-tier mind: live + project context survives `/clear`, `/compact`, suspend, and cross-node resume. The single biggest continuity win — wire the directory watch and read the contract filename |
| **Resource advertisement** (`[session] resources` blurb / `spt endpoint description`) | A free-text "what I can serve" string riding the endpoint's registry rows | Other agents discover the endpoint's capabilities (`spt resources list`) instead of guessing |
| **Install-on-demand bootstrap** | Pack the check-and-install of spt-core into your harness's first run (the [bootstrap pattern](install-on-demand.md)) | Zero-friction first run — the user installs your harness, spt-core comes with it |
| **Surfacing `spt how-to <topic>` to the agent** | Let the agent read task-oriented spt-core guidance from the binary itself | The agent self-serves common operations (subnet join, sending) instead of asking the user |
| **Presence-driven idle reporting** | Fire `api state idle` from a *real* user-inactivity signal, not a timer | Accurate dormancy → Psyche wakes on genuine activity, echo-communes fire at true boundaries |

**claude-code (the worked example):** ships the modern two-tier mind end to
end — the session drops `<id>-commune.md` at every `/clear` and `/compact`, and
a Self-authored `<id>-signoff.md` at graceful stop, into the watched
`commune_dir`; declaring `[session.psyche_init]` promotes the endpoint to a
LiveAgent (a **go-live gate** — spt-core never spawns it), and the
`[session.psyche_resume]` per-event turn (+ the `[session.echo_commune]`
template) lets spt-core drive the Psyche that ingests them; `[update] avenue =
"delegated"` makes the Claude Code plugin updater the
ripple avenue. That is the bar a native-feeling harness clears.

---

## Patterns introduced in v0.16.0

### Hook dispatch by resolve-not-execute

spt-core never grows a hook-**execution** surface — `[hooks.<event>]` stays
purely outbound (the harness fires `fires`; spt-core never runs a hook handler).
When your hook *logic* must live in an adapter binary (so it rides
`spt adapter update`) but the harness loads hooks from a static plugin dir, use
the two adapter-static substitution keys to resolve+run **your own** binary:

- **`{adapter_dir}`** fills to your install dir (the registry `source_dir`) and
  **survives updates**; **`{adapter_name}`** fills to your adapter name. Both are
  available wherever substitution runs — including, new in v0.16.0, **inside
  `[strings]` values at `get-string` read time** (scoped to *just* these two
  adapter-static keys; `get-string` has no session context, so `{id}`/
  `{session_id}` are not available there).
- Store the dispatch command in `[strings]`:
  ```toml
  [strings]
  hook_cmd = "{adapter_dir}/claude-spt hook"
  ```
- A thin, static per-OS dispatch wrapper (the one plugin-resident piece) runs
  `spt adapter get-string <adapter> hook_cmd` **once per session** (memoize the
  resolved string into an env var for a hot-path hook like PostToolUse), then
  executes the resolved command per-hook itself. spt-core only **resolves and
  returns** the string — it never executes it (ADR-0029).

**claude-code:** the plugin ships a static `hooks.json` + a per-OS dispatch
wrapper; the wrapper resolves `get-string claude-spt hook_cmd` →
`<install_dir>/claude-spt hook` once per session and runs it per-hook, so all
hook logic updates via `spt adapter update claude-spt`.

### Incremental digest consumption — the `--json` cursor

`spt endpoint digest <id> --json` supports turn-end incremental consumption
(v0.16.0): `--last <N>` (the last N turns; `--last 1` = the latest turn), a
stable per-entry **`seq`** (source-derived — re-projection yields the same `seq`;
it does not renumber when the window slides), and `--after <seq>` (entries newer
than `seq` still in the window; a full-window refresh + a predates signal if
`seq` has fallen out). The trailing in-progress turn is flagged `partial: true`
and its entries carry no stable `seq` until the turn closes (a turn is bounded by
a user-input) — a consumer reprocesses `partial` and skips entries `<= seq`.
`seq` is the authoritative dedup + cursor key.

#### The `--json` output shape

The snapshot is one pretty-printed JSON object; `--follow --json` emits one
**compact** JSON object per line (a delta stream). Shapes as of v0.26.0:

```json
{
  "turns": [
    {
      "input": "fix the bug",
      "input_seq": 4294967296,
      "entries": [
        { "Agent":      { "text": "on it", "seq": 4294967297, "ts": "2026-07-06T09:00:00Z" } },
        { "ToolSprint": { "tools": [ { "name": "Write", "arg": "src/a.rs" } ], "seq": 4294967298 } },
        { "Boundary":   { "kind": "clear", "ts": "2026-07-06T09:05:00Z" } },
        { "Context":    { "kind": "owl_message", "body": "<EVENT type=\"msg\" from=\"lea\">ping</EVENT>", "ts": null } }
      ]
    }
  ]
}
```

- **Turn**: `input` is the opening user-input text, `null` for a preamble
  turn (boundary/context entries that precede any input). `input_seq` and
  `partial` are **omitted** when absent/false — a trailing open turn carries
  `"partial": true` and its entries carry no `seq`.
- **Entries are tagged by kind** — each entry is a one-key object whose key
  is the kind. The closed kind set: `Agent` (`text`, optional `seq`/`ts`),
  `ToolSprint` (`tools`: `{name, arg}` in order, `arg` presentation-truncated;
  optional `seq` = the **last** collapsed record's, optional `ts`),
  `Boundary` (`kind`: `clear` | `compact` | `boot`), and `Context`
  (`kind`: `psyche_download` | `echo_mirror` | `owl_message`, plus `body`).
  `Agent`/`ToolSprint` omit `seq`/`ts` when absent; `Boundary`/`Context`
  carry no `seq` (they are spt-injected, not transcript records) and their
  `ts` is present-but-`null` when unknown.
- **A delivered message can appear twice by design**: as the turn-opening
  `input` (your extractor's classification, below) *and* as a
  `Context`/`owl_message` row whose `body` is the **whole composed `<EVENT>`
  envelope verbatim** — exactly what the agent saw. Parse it with the
  [envelope rules](../messaging/overview.md#the-event-wire-contract); it is
  the message-identity anchor for dedup across the two appearances.
- **`--after` signal**: when the cursor predates the window, the snapshot is
  the full window plus a top-level `"after_predates_window": true`.
- **stderr trailer**: every successful pull prints `DIGEST:<id> version=<n>`
  on stderr (the version pairs with the delta stream below); an endpoint with
  no activity buffer reports `NO_DIGEST:<id>` and exits non-zero.
- **`--follow --json` delta lines**: `{ "version": <n>, "from": <i>,
  "turns": [ … ] }` — apply by truncating your view to `from` turns and
  appending; `from == 0` is a full replace (the base snapshot, or a window
  slide). Deltas are sent only when the digest actually changed.

**Binding for your `[digest]` extractor / `api digest-entry`:** classify a
**delivered user-facing message as a turn-opening `input` record** (equivalent to
a direct PTY user-input). The projection treats `role: "input"` as the turn
boundary; if messaging-delivered turns are not opened as `input`, a
messaging-driven session collapses into a few giant turns and `--last`/`seq` lose
granularity. *What* becomes `input` is your call; *that it opens a turn* is the
contract.

### Global `--json` for read/status commands

The read/status command set (`endpoint list`/`whoami`, `daemon status`, `subnet
status`/`show-code`, `endpoint description`/`role`, `adapter list`/`version`,
`notif list`, `grant list`, `access list`, `shell list`, `how-to`) honors a
global **`--json`** flag (v0.16.0) for scripted consumption — stable, explicit
per-command field names (a committed wire-parity surface). Action commands ignore
it. Flag reference: the [CLI reference](../cli/reference.md).

Committed compatibility posture for every `--json` shape: **additive evolution** —
new fields appear (often omitted-when-absent), existing fields are never renamed
or re-typed; parse tolerantly (ignore unknown keys).

#### `endpoint list --json` — the output shape

One object, three sections (as of v0.27.0):

```json
{
  "self": {
    "id": "doyle",
    "status": "live_agent",
    "ready": true,
    "alive": true,
    "unbound": false,
    "description": null,
    "psyche_host_error": null
  },
  "subnets": [
    {
      "name": "home",
      "endpoints": [
        {
          "id": "flynn",
          "node": "1a2b3c…",
          "node_label": "HFENDULEAM",
          "status": "Active",
          "resources": null,
          "endpoint_type": "live_agent",
          "project": "spt-mobile"
        }
      ]
    }
  ],
  "local": [
    {
      "id": "doyle",
      "state": "live_agent",
      "address": "127.0.0.1:52110",
      "ready": true,
      "alive": true,
      "unbound": false,
      "project": "spt-core"
    }
  ]
}
```

- **`self`** — the calling session's own endpoint, `null` when the session has no
  perch. `status` is the local perch state token (`live_agent`, `ready_agent`, …;
  `null` with no local perch), `ready`/`alive` likewise `null` for a pinless
  session. `description` is the endpoint's authored description or `null`.
  Two fault annotations are **omitted entirely when absent**:
  `psyche_host_error` (string) and `translation_fault` (string) — presence means
  the human view shows the same fault line.
- **`subnets`** — one group per subnet the node belongs to, `endpoints` from the
  subnet's gossip projection. `status` is the ADVERTISED cross-node state, closed
  set: `Active` | `Dormant` | `Suspended` | `Offline`. `node` is the hosting
  node's key prefix, `node_label` its display label (or `null`). `resources` is
  the endpoint's advertised description string (or `null`). `endpoint_type`
  (`live_agent`, `ready_agent`, …) and `project` (latest project id) are
  **omitted when absent** — older rows may not carry them.
- **`local`** — this node's perches from the roster. `state` is the same token
  set as `self.status`; `address` is the listener address (or `null`);
  `project` omitted when absent.
- **Filters apply before serialization**: worker endpoints are excluded from all
  three sections by default (v0.27.0) — pass `--workers` to include them;
  suspended rows honor `--all` the same way. `spt whoami --json` emits its OWN
  identity-only shape *(since v0.33.0 — previously this list shape)*:
  `{id, state?, ready?, alive?, unbound?, description?}`, or `{"id": null}` +
  exit 1 when the session owns no endpoint. It never derives projects — the
  bounded-time identity verb for hook paths (see the [API reference](api.md)
  Introspection section).

---

## "Am I done?" — the floor

- [ ] Manifest validates against
      [`manifest.schema.json`](http://localhost:5474/manifest.schema.json)
- [ ] `[adapter]` header complete (`name`, `kind`, `version`,
      `min_spt_core_version`, `hostable_types`)
- [ ] One startup flow wired: `SessionStart → seed` + `listen`
      (harness-hosted) **or** `[session.self]` + `bind` (spt-hosted)
- [ ] (harness-hosted) `[adapter] host_binaries` names your harness exe(s) so
      `seed`/`listen` resolve with no `--adapter`; `spt adapter use <adapter>` sets
      the active default when several adapters host the same binary
- [ ] `api state idle` fires on real inactivity; `can_inject` values are honest
- [ ] An inbound delivery channel is declared (`[inject]`) or pulled (`api poll`)
- [ ] `[history]` strategy chosen; `api boundary` wired for clear/compact
- [ ] (mind continuity) SessionStart fires `api psyche-download` and injects
      its stdout, so a resumed session gets its durable context back
- [ ] (for a live digest) `[digest]` extractor declared + `digest-proof`-checked, or `api digest-entry` push
- [ ] (spt-hosted, if your harness resumes by id) `[session.resume]` declares the native-resume command — else a resume comes up blank
- [ ] (spt-hosted, for idle message delivery) `[message-idle-translation-binary]` declared + `translate-proof`-checked, or accept the degenerate `payload+enter` inject
- [ ] `[update]` avenue declared (ripple-update + install-on-demand)
- [ ] Teardown fires `api session-end` (or `api shutdown` for graceful signoff)
- [ ] **Recommended:** commune/signoff directory watched (mind continuity)
- [ ] `spt adapter add ./your-adapter` registers clean; `api … capability`
      echoes your `hostable_types`

## Next

- **Reference:** the complete [manifest reference](manifest.md) and
  [`spt api` reference](api.md).
- **Ship it:** the [install-on-demand bootstrap](install-on-demand.md).
- **Driven surfaces:** [Shells](../shells/overview.md) — the `kind = "shell"`
  flavor of this same contract.

===== /harness-contract/manifest.md =====

# Manifest reference

The runtime manifest is the declarative half of the harness contract: one TOML
file per adapter, declaring **only what varies per harness or shell**. This
page is the complete field reference.

Machine-readable companion: [`manifest.schema.json`](http://localhost:5474/manifest.schema.json)
— generated from the exact code that parses manifests, so it never drifts.
Validate your manifest against it, then `spt adapter add` enforces the
cross-field rules listed at the bottom.

## The principle

**SPT is not a harness.** Command templates are opaque strings — spt-core
never parses out a model, tool list, or flag; the adapter writes the full
command line and spt-core runs it with `{key}` substitution placeholders
filled. Anything spt-core owns is *not* in the manifest:

- **Sentinels** (idle markers, the echo gate) — managed via `spt api state` /
  `spt api echo-gate`; adapters only call them.
- **Spool, registry, perch, and daemon-state schemas.**
- **The event-block vocabulary** — the tags spt-core surfaces to agents are a
  fixed, documented constant. Adapters pass spt-core's output through
  unchanged.
- **File-drop filenames** — statically `<endpoint_id>-commune.md` /
  `<endpoint_id>-signoff.md`; only the watched *directory* is declared.
- **Config knobs** (pulse period, summarizer windows, …) — global spt-core
  settings with per-endpoint overrides, never per-adapter.

## Substitution keys

The full `{key}` vocabulary spt-core fills into command templates. A role's
`keys` list must be a subset of this catalog, and every `{placeholder}` in a
`command` (or `cwd`/`source`/`fires`/…) must resolve to a value spt-core
supplies for that spawn — an unknown or unprovided key fails with a one-line
error. Not every key exists in every context: spt-core fills only those
relevant to the spawn (e.g. `{psyche_*}` only for a live agent's Psyche role,
`{source}` only for a `[digest]`/`[history]` extractor).

| Key | spt-core fills it with |
|---|---|
| `{id}` | The endpoint id being hosted. For a **Psyche** role this is the **parent endpoint id** (the LiveAgent being hosted), not the nested `<parent>-psyche` perch id. |
| `{adapter_name}` | The adapter's declared `name` (the value every `api` call carries). |
| `{adapter_dir}` | The adapter's install dir (the registry record's `source_dir`) — adapter-static, available wherever substitution runs (every `[session.*]`, `[digest]`, the translation binary, and lazy `[strings]`). Survives updates; lets a command point at the adapter's own packed binary (resolve-not-execute, ADR-0029). |
| `{session_id}` | The harness session id (minted at spawn; reported back via `api seed`). For a **Psyche** role this is the Psyche's **own** custody session id — its own conversational thread, which a parent boundary (`/clear`, `/compact`) does not rotate — never the parent's. |
| `{parent_session_id}` | The **parent** session id, exposed under its own explicit key so a Psyche role template that needs the parent's id never aliases `{session_id}` (which on a Psyche spawn is the Psyche's own custody id). |
| `{session_name}` | The session's display name, when one is supplied. |
| `{node}` | This node's **advertised label** — the value the roster and picker render (its OS hostname, read into the label store at daemon startup), never the pubkey. Node-static: available wherever the session keys populate **and** in lazy `[strings]` resolution. **Single-token fill only** (a space-carrying label stays adapter-shim territory); when no label is known the key is left unfilled so a referencing template fails loudly, never an empty token. Note: the daemon-side lifecycle resolves it once at startup while a CLI-originated spawn reads a fresh hostname, so the two differ only across a mid-life hostname change. |
| `{subnet}` | This endpoint's **home-subnet label** (`local` when unhomed), filled whenever it is known so a nested Psyche turn need not resolve a `--subnet` it cannot know. A single `{subnet}` concept — there is deliberately no `{home}` key. When no subnet is known the key is left unfilled so a referencing template fails loudly (the `{node}` precedent). |
| `{parent_pid}` | The harness parent process pid — the SessionStart `api seed` anchor. |
| `{agent_type}` | The hosted agent type. |
| `{psyche_context_file}` | The **path** to the file spt-core writes the Psyche's carried context into before each turn (never the context body on the argv — a large mind would exceed the command-line length cap). Fresh vs continue is the file's **content**: a fresh/reseeded turn writes it non-empty, a continue turn writes it 0-byte. Replaces the former `{psyche_context}` body key. |
| `{link_token}` | A shell-link capability token (shell adapters). |
| `{source}` | The transcript/log path spt-core resolves for a `[digest]`/`[history]` extractor. |

## `[adapter]` — header (required)

The only mandatory section, and it must be readable *before* any install or
update — `min_spt_core_version` is the compatibility gate.

```toml
[adapter]
name = "my-harness"                # the adapter_name; an optional --adapter override
kind = "harness"                   # "harness" (default) | "shell"
version = "1.0.0"
min_spt_core_version = "1.0.0"     # lowest spt-core this adapter tolerates
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
host_binaries = ["my-harness"]     # harness exe(s) you host → bind-time resolution, no --adapter
```

| Field | Required | Meaning |
|---|---|---|
| `name` | yes | Adapter id; the value an optional `--adapter <name>` override carries |
| `kind` | no (default `harness`) | `harness` hosts agents; `shell` provides a driven surface |
| `version` | yes | The adapter's own version |
| `min_spt_core_version` | yes | Compat gate, checked before install/update |
| `hostable_types` | no | Endpoint types this adapter can host |
| `host_binaries` | no (harness) | Harness exe basenames you host — the bind-time match-key so `seed`/`listen` resolve with no `--adapter` (since v0.9.0). Matched on **lowercase + stem-before-first-dot**, so `claude` matches `claude`/`claude.exe`/`claude.cmd`/`claude.exe.old.<ts>` (a self-update can rename the running exe); a declared name must not contain a dot |

## `[hooks.<event>]` — inbound hook table

One entry per harness event, declaring the `spt api` command it fires, the
input fields it maps in, and whether the hook can surface text into the
agent's context.

```toml
[hooks.SessionStart]
fires = "api seed --pid {parent_pid} --session-id {session_id}"   # adapter-agnostic since v0.9.0
reads = ["session_id", "parent_pid"]
can_inject = true

[hooks.Stop]
fires = "api state idle"
can_inject = false     # no inject channel -> sentinel/relay fallback
```

| Field | Required | Meaning |
|---|---|---|
| `fires` | yes | Opaque `api …` command line the harness invokes for this event |
| `reads` | no | Input fields (e.g. from the hook's stdin payload) mapped into the command |
| `can_inject` | no (default `false`) | Whether this hook can inject context back to the agent. When `false`, spt-core falls back to its sentinel + relay/poll path instead of expecting injection |

`can_inject` is the single most load-bearing harness-varying fact — declare
it honestly per hook.

## `[session]` — watched dirs + role templates

Two watched-directory keys sit directly on `[session]`; the file *names* are
fixed by spt-core, only the directory varies:

```toml
[session]
commune_dir = ".my-harness"    # watched for <endpoint_id>-commune.md
signoff_dir = ".my-harness"    # watched for <endpoint_id>-signoff.md
```

Commune and signoff are **file-drops, not commands** — an agent writes a
markdown file; spt-core's watcher does the rest.

### `[session.<role>]` — outbound templates

One opaque command template per role. Model, tools, flags, permissions — all
live inside `command`, never as separate fields.

Roles: `self` (the agent's own session) · `resume` (the agent's own-session
**native resume**, the `self` sibling) · `psyche_init` (**go-live gate only** —
its presence promotes the endpoint to a LiveAgent; spt-core never spawns it) ·
`psyche_resume` (the **sole driven** Psyche role — one bounded per-event turn) ·
`echo_commune` (the bounded history summarizer for sessions that end without a
signoff) · `signoff` (final context save) · `notif` (endpoint-native
notification render).

<!-- [doc->REQ-SESSION-RESUME-TEMPLATE] -->
**Resuming an existing harness session (since v0.13.0).** `[session.self]` is the *fresh*
bringup; `[session.resume]` is the **native-resume** sibling. spt-core selects
`[session.resume]` over `[session.self]` only when a bringup carries a prior
session (`spt endpoint run --resume <session>`, or the picker's *Resume from
history*) **and** your manifest declares the role. Declare it with your
harness's native-resume verb — if your harness resumes a transcript by id, use
that form (Claude Code: `claude -r {session_id} …`), **not** the fresh
create-session form. Skip the role and a resume silently re-runs `[session.self]`
(a *fresh* session → a blank transcript). spt-core fills the SAME key catalog as
`self` (`{id}`, `{session_id}` = the **resumed** id, `{session_name}`,
`{adapter_name}`) and lands the PTY in the session's recorded **project cwd** (a
harness resolves a transcript by `session_id` **+ cwd**) — the per-session
ledger row's cwd, else the endpoint's bind cwd, else the current dir.

```toml
[session.resume]
command = "my-harness resume --session {session_id} --id {id}"
keys = ["session_id", "id"]
```

```toml
# Go-live gate ONLY — spt-core never spawns this; its presence makes the endpoint live.
[session.psyche_init]
command = "my-harness run --agent psyche --model cheap"

# The role spt-core actually drives — one bounded turn per Psyche event.
[session.psyche_resume]
command = "my-harness run --agent psyche --resume {session_id} --model cheap"
env_remove = ["MY_HARNESS_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"
keys = ["session_id", "parent_session_id", "psyche_context_file", "subnet"]
```

A Psyche runs as a **bounded per-event turn**, not a resident process (there is
no psyche pid to poll — liveness is that turns succeed). Declaring
`[session.psyche_init]` is the **go-live signal only** — spt-core **never spawns
it**; the per-event turn drives **`[session.psyche_resume]` exclusively**. For
that turn spt-core fills `{session_id}` (the Psyche's **own** custody id — see
the key table), `{parent_session_id}`, `{psyche_context_file}`, and `{subnet}`
(when known); the adapter-static/node keys `{id}` (the **parent endpoint id**),
`{adapter_dir}`, `{adapter_name}`, and `{node}` are also available. It does
**not** fill `{session_name}` (a `[session.self]` key). Declaring a key your
role's spawn isn't given fails at spawn, so template only the keys spt-core
fills for the role.

**Shipped binaries resolve from the install dir (since v0.8.0).** A command
template's bare program token (its first token, e.g. `my-harness-digest`)
resolves against the adapter's **install dir** before `PATH`, so a `.spt` that
ships its own binaries is self-contained — no PATH placement needed. spt-core
runs `<install_dir>/<program>` (on Windows also trying the `.exe` suffix) when
that file exists, else falls back to `PATH`. The install dir is where your
adapter was registered (the `--release`/`--github` durable home, or the
copy-mode source dir). This applies to the `[session.psyche_resume]` per-event
turn, the [`[digest]`](#digest--session-digest-extractor) extractor, and
`spt adapter digest-proof`. Ship a binary in your `.spt` and reference it by
bare name; you need not place it on `PATH`.

| Field | Required | Meaning |
|---|---|---|
| `command` | yes | Opaque command line with `{key}` placeholders |
| `cwd` | no | Working directory (substitutable) |
| `recursion_guard_env` | no | Env var set on summarizer children so *their* hooks bail (no summarizer-of-summarizer loops) |
| `detach` | no (default `false`) | Spawn detached |
| `env_remove` | no | Env vars stripped from the child's inherited environment |
| `keys` | no | The substitution keys spt-core fills for this role |

`notif` is the endpoint-native notification render — an OS toast, a status
LED, anything the adapter can run. Spawned detached when a notification
surfaces at this endpoint. Keys spt-core fills: `{notif_id}`, `{notif_from}`,
`{notif_subnet}`, `{notif_body}`.

```toml
[session.notif]
command = "powershell -Command New-BurntToastNotification -Text '{notif_from}','{notif_body}'"
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
```

## `[env.<VAR>]` — env-var table

Vars to inject into (or read from) sessions, and how. The injection channel is
asymmetric by hosting mode: **spt-hosted** sessions inherit env from the
broker that spawned them (no channel needed); **harness-hosted** sessions need
the harness's declared channel.

```toml
[env.MY_HARNESS_SESSION_ID]
direction = "inject"        # "inject" | "read"
value = "{session_id}"      # required for inject
channel = "MY_ENV_FILE"     # harness-hosted only
```

## `[history]` — transcript access

How spt-core reads a session's conversation history (it powers the
echo-commune summarizer). Three strategies; pick exactly one:

```toml
[history]
strategy = "fetcher"      # "fetcher" | "locate_normalize" | "native"
fetcher = "my-harness-history --session {session_id}"
```

| Strategy | Required fields | Meaning |
|---|---|---|
| `fetcher` | `fetcher` | spt-core runs your binary; it emits normalized history |
| `locate_normalize` | `locate_template`, `normalize_command` | spt-core locates the raw transcript, then runs your normalizer over it |
| `native` | — | The adapter pushes via `spt api history-log`; spt-core stores it |

spt-core has **no built-in transcript parser for any harness** — the adapter
always owns that knowledge.

## `[digest]` — session-digest extractor

The session digest's own seam (ADR-0019) — separate from `[history]`, which stays
opaque and single-session for the echo-commune. `[digest]` declares an
**imperative extractor** that maps your harness's native log to the digest-record
contract:

```toml
[digest]
extractor = "my-harness-digest --session {session_id} --in {source}"
source = "~/.my-harness/{session_id}.jsonl"   # optional; defaults to [history].locate_template
window_turns = 5         # optional presentation defaults you declare…
arg_truncation = 40      # …any consumer may override at pull/subscribe
sprint_collapse = true
```

| Field | Required | Meaning |
|---|---|---|
| `extractor` | yes | Opaque command: native log → contract JSONL (one record/line). Under `locate_normalize` spt-core fills `{source}` with the resolved path and pipes the bytes on stdin; under `fetcher` it just runs the command and reads stdout. |
| `strategy` | no | Which side locates the transcript, mirroring `[history]` — `locate_normalize` (default) or `fetcher`. See the strategy table below. |
| `source` | no (locate_normalize only) | Own-source log path; absent, reuse `[history].locate_template`. Under `locate_normalize` one of the two **must** resolve, else `spt adapter add` rejects (see [Cross-field rules](#cross-field-rules-spt-adapter-add-enforces-these)). Ignored under `fetcher`. |
| `window_turns` / `arg_truncation` / `sprint_collapse` | no | Adapter-declared presentation **defaults**; any consumer may override. spt-core fallback: `3` / `25` / collapse-on. |

<!-- [doc->REQ-DIGEST-FETCHER-STRATEGY] -->
`[digest]` supports the same two locate strategies as `[history]` — pick with `strategy`:

| Strategy | Who locates the transcript | `source` |
|---|---|---|
| `locate_normalize` (default) | **spt-core** resolves the single `source` file, reads it, pipes the bytes to the extractor on stdin. | Required (own `source` or inherited `[history].locate_template`). |
| `fetcher` | **The adapter's** extractor locates + reads + emits; spt-core runs it bounded and consumes stdout — no `source`, no pre-read. | Not used. |

Use `fetcher` when the transcript lives in a **partitioned** layout spt-core cannot
name with one template — e.g. Claude Code's `projects/<munge(cwd)>/<session_id>.jsonl`
or a date-globbed rollout tree. spt-core feeds the extractor only the
harness-**neutral** inputs it owns — `{session_id}`, the perch-bound `{cwd}`, and any
captured [`[env] direction = "read"`](#envvar--env-var-table) vars (e.g.
`{CLAUDE_CONFIG_DIR}`) — never a harness-specific project slug; the extractor globs
the unique `{session_id}` under the root itself:

```toml
[digest]
strategy = "fetcher"
extractor = "my-harness-digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR} --cwd {cwd}"
# no `source` — the extractor locates the file
```

Why a command, not a declarative map: real harness logs are nested (one line →
many entries, mixed block lists, types to filter); a flat map can't express them.
A **log-less** adapter declares no `[digest]` and pushes via `spt api
digest-entry` instead. Validate before shipping with `spt adapter digest-proof
<adapter> --sample <real-log>`. `digest-proof` fills the same `{id}` and
`{session_id}` the runtime `endpoint digest` does, so a `{session_id}`-templated
extractor (e.g. `--session {session_id} --in {source}`) proofs exactly as it
runs live; pass `--session <id>` to pin a specific session id.

## `[inject]` — input-injection methods

How text can be put in front of the agent, per activity state. Any
combination of `pty`, `hook`, `relay`, `http`:

```toml
[inject]
activity = ["hook"]            # non-disruptive while the agent is working
idle = ["pty", "hook"]
```

## `[message-idle-translation-binary]` — spt-hosted idle delivery

<!-- [doc->REQ-MSG-IDLE-TRANSLATION-BINARY] -->

**Opt-in, spt-hosted only (since v0.13.0).** An adapter's **idle-delivery translation binary**: a
pure `stdin → stdout` JSON-lines filter spt-core lifecycle-manages (spawned when
the spt-hosted endpoint comes up, terminated when it goes down). spt-core feeds it
the inbound `<EVENT>` message feed and reads back keystroke-commands, which it
applies to the broker-held PTY **atomically** — a live `spt rc` controller's input
is buffered during the emitted sequence and flushed after, so idle injection
coexists with an attached operator (spt-core owns every PTY write). **Idle delivery
only** — busy / mid-turn delivery stays your `[inject]` hook path.

Declared as a **table** carrying a `path` scalar (a table can't be silently
absorbed by a preceding section and stays extensible):

```toml
[message-idle-translation-binary]
path = "cc-spt-idle-translate"     # the binary spt-core spawns + drives
```

- **stdin** (spt-core → binary, one JSON object per line): `{"type":"init","endpoint_id":…,"node":…}` first · `{"type":"event","envelope":"<EVENT…>"}` per inbound message (the `<EVENT>` envelope) · `{"type":"input"}` — a **content-free** ping each time the operator types, so the binary can track user-idle (the PTY input content is **never** duplicated to the binary).
- **stdout** (binary → spt-core, one per line): `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<payload>"}` · `{"key":"enter"}` · `{"commit":true}`, … (extensible vocabulary).
- **`{"commit":true}` is the mandatory sequence terminator — and you MUST send it for EVERY `{"type":"event"}`.** While your emitted sequence is in flight, spt-core buffers a live `spt rc` controller's keystrokes (the *inject floor*) and applies your commands to the PTY atomically; `{"commit":true}` — emitted as the **last** record — releases that floor and flushes the buffered controller input *after* your sequence. The submit keystroke is **not** the terminator: `{"key":"enter"}` (or a trailing `\r` inside a text payload) submits the input, but a choreography may keep typing *after* it (e.g. a stash/restore that presses a key after submitting), so commit is a distinct, explicit signal you always send last. **An empty response is a protocol violation:** even when you have nothing to inject (an event with nothing armed, or an event without an envelope), you MUST still answer with at least a bare `{"commit":true}` — a response of zero records is treated as a missed commit.
- **Missed commit → the sequence is tolerated, not fatal.** If no `{"commit":true}` arrives within the **commit deadline (5 s)**, spt-core still flushes the buffered operator input (never stranded) and re-spools that one message once so it is not lost — but it does **NOT** terminate a healthy binary. A single miss is tolerated; the binary is preserved and the next event delivers through it as normal. Only after **3 consecutive** missed commits (a genuinely wedged binary), or a real binary death, does spt-core fault the binary — and even then it **bounded-eager-respawns** it (a healthy commit resets the budget) rather than leaving it permanently dead, surfacing the fault on the endpoint's status while it is degraded. (This supersedes the pre-v0.14.3 "falls back to a raw inject" behavior — raw inject was removed; a missed commit never types your payload raw.)
- Unknown fields are **not** rejected here — a newer adapter declaring a future key against an older spt-core parses fine (the key is ignored), so the contract degrades gracefully.
- `{"text":…}` is applied to the PTY **verbatim** — bytes are typed exactly, with **no** control-character stripping. A trailing `\r` *inside* a text payload (`{"text":"…\r"}`) therefore **submits**, identical to a following `{"key":"enter"}` (`enter`→`\r`). Submit either way; just don't do both. Corollary: neutralize any CR/LF *inside* the message body before the trailing submit, or an embedded newline fires the input early.
- A minimal binary just emits `{"text":payload}{"key":"enter"}{"commit":true}` with no choreography. (spt-hosted idle delivery is translation-binary-only since v0.14.3; there is no raw-inject fallback — a binary that fails to spawn or misses its commits spools the message, it is never typed raw.)

## `[identity]` — session identity

How the harness's session id is obtained:

```toml
[identity]
session_id_source = "post_spawn"   # "post_spawn" | "uuid_inject"
parent_ancestor_name = "my-harness"
```

`post_spawn`: discovered after spawn (process tree / wrapper hand-off), with
`parent_ancestor_name` as the process-tree anchor. `uuid_inject`: spt-core
injects a UUID the harness echoes back.

## Session digest — the digest-record contract

The live activity digest (`spt endpoint digest <id>`) is a **projection of the
endpoint's session logs**, not a parse of the PTY byte stream. Your `[digest]`
extractor (or a `spt api digest-entry` push) emits the **digest-record contract** —
JSON objects spt-core projects:

```json
{"role": "input", "text": "add a file", "ts": "2026-06-13T21:00:00Z"}
{"role": "agent", "text": "on it"}
{"role": "tool",  "tool": {"name": "Write", "arg": "src/a.rs"}}
```

- `role` ∈ `input` | `agent` | `tool` (the source tag).
- `text` — the input / agent span (omitted for `tool`).
- `tool` — `{name, arg}`, present iff `role == "tool"`; consecutive tool records
  collapse into one sprint (unless `sprint_collapse = false`).
- `ts` — optional RFC3339-UTC ordering key (used to interleave with spt's own
  injected-context entries).

Unknown fields are ignored; a line that isn't a valid record is **dropped with a
counted reason** (never silently). `spt adapter digest-proof` shows you exactly
what dropped and why. Presentation (window depth, arg truncation, sprint collapse)
is spt-core's, defaulted by your `[digest]` and consumer-overridable; extraction is
yours.

## `[strings]` — adapter string values (+ profiles)

An adapter-authored key/value tree any process on the node reads by dot-path with
`spt adapter get-string <adapter[:profile]> <key.path>` — e.g. a harness hook
fetching per-profile `additionalContext`, so one hook script serves every profile
and only the data differs. **Strings are data only** — spt-core never executes a
string (command templates live in the typed sections, never here). Node-local; not
cross-node synced.

```toml
[strings]
greeting = "hello"                       # inline literal
skills.whoami = { file = "whoami.md" }   # file pointer → resolved to the file's contents
```

**Two value forms:**
- **Inline literal** — `get-string` prints it as-is.
- **File pointer** — a value-position table with **exactly one** key, `file`:
  `{ file = "rel/path" }`. `get-string` resolves it to the file's **contents** (large
  bodies — skill instructions, hint text — stay out of the manifest). The
  exactly-one-key rule disambiguates: any other table shape stays an opaque nested
  strings tree, and `{ file = … }` is **reserved** as the pointer form (it can't
  double as inline data).

**File-pointer rules (since v0.7.0):**
- Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`**
  (sibling of `profiles/`); the path is **relative to that dir and must stay inside
  it** — `..` traversal and absolute paths are refused at registration
  (`ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer … must be a relative
  path inside the strings/ dir (no absolute paths, no `..` traversal)` — manifest-first,
  so the whole add registers nothing).
- **Validated at registration** (fail-fast on an escaping/missing pointer), **read
  lazily** at `get-string` so live file edits reflect without re-register. A
  missing/unreadable file at read time **skip-diagnoses** — a diagnostic plus
  "not set", never a silent drop or hard error (mirrors `[digest]`).
- On `spt adapter add`, the adapter dir is **copied** into the registry
  (`adapters/<adapter>/{manifest.toml, record.toml, strings/…}`).

**Profiles + update-safety:** strings resolve through the same **leaf-replace**
profile overlay as the rest of the manifest — a shipped or local profile may override
base strings, and `get-string <adapter:profile>` returns the merged view. A **local**
profile's own file pointers resolve against the **user-owned local-profile dir**, not
the adapter-shipped `strings/` (which adapter updates overwrite) — so a local override
survives updates (or a local profile may just inline a literal). `set-string` edits a
**local** profile's `[strings]` only, never adapter-shipped files.

## `[update]` — adapter self-update

<!-- [doc->REQ-ADAPTER-UPDATE-MESSAGE] -->

How spt-core updates (and first installs — install is the first update) this
adapter:

```toml
[update]
avenue = "delegated"                      # "delegated" | "file_pull" | "gh_release"
command = "my-harness plugin update spt"  # delegated: the updater to run
self_verifies = true                      # delegated: attests the updater verifies its content
uninstall = "my-harness plugin uninstall spt"   # optional inverse, run by `spt adapter remove`
message = "Run `/reload-plugins` in any ongoing sessions."   # optional; shown on apply
```

| Avenue | Required fields | Meaning |
|---|---|---|
| `delegated` | `command` | spt-core delegates to the harness's own updater. Set `self_verifies = true` to attest that updater verifies what it installs — an unattested delegated update is skipped as unverifiable |
| `file_pull` | `repo`, `signing_key` | spt-core pulls files from `repo` (optionally filtered by `path_regex`) and verifies them against the adapter author's Ed25519 `signing_key` (64 hex chars) before applying |
| `gh_release` | `repo` | spt-core ships your updates from your own GitHub releases (since v0.8.0). `asset` (default `adapter.spt`) and `signing_key` are optional |

**`message`** (optional, any avenue) — a plain human notice `spt adapter update` prints to
stdout, markdown-rendered, **only when a new version is actually applied** (never on a
no-op). Printed after the update completes; multi-line supported. No `{key}`
substitution. Use it to tell the operator what to do after updating — e.g.
`"Run \`/reload-plugins\` in any ongoing sessions."` for spt-claude-code.

With `file_pull`, **you** sign your releases with your own key; spt-core's
release keys never extend to adapter content.

### `gh_release` — ship updates from your GitHub releases (since v0.8.0)

The simplest avenue to publish for: distribute exactly as you do for
`spt adapter add --release`, and your registered adapter stays current.

```toml
[update]
avenue = "gh_release"
repo = "your-org/your-adapter"   # required: whose releases ship updates
asset = "adapter.spt"            # optional: the release asset to fetch (default adapter.spt)
signing_key = "deadbeef…"        # optional Ed25519 (64 hex): enables fail-closed verify
```

`spt adapter update [name]` (no name sweeps every registered `gh_release`
adapter; a name updates just that one) compares your repo's latest release
version against the installed one and, when newer, fetches the release `.spt`
archive — the same archive `spt adapter add --release` installs — then
re-extracts and re-registers it. `repo` is the only required field.

**Trust is opt-in signing, fail-closed.** Declare no `signing_key` and the
fetched `.spt` is trusted on HTTPS + GitHub, exactly like first acquisition.
Declare a `signing_key` and the fetched `.spt` is verified against a **detached
signature** you publish as a sibling release asset named `<asset>.sig` — a
lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs
after the archive is fetched and before it is extracted, against the key in the
**installed** manifest (so a new release must verify against the key already on
the node). A bad or missing signature refuses the update and the fetched bytes
are discarded, never extracted. You sign your own releases with your own key;
spt-core's release keys never extend to adapter content.

### `[update.post]` — the composite post-step (since v0.16.0)

<!-- [doc->REQ-ADAPTER-UPDATE-POST] -->

An optional, avenue-agnostic second step spt-core runs **after** the primary
avenue resolves — one lever pulls your `.spt` **and** runs your in-harness
reconcile (e.g. a plugin updater):

```toml
[update.post]
command = "{adapter_dir}/reconcile --sync-plugin"  # required; {key} substitution + program-token
                                                   # resolution against the install dir
self_verifies = true                               # attestation, mirrors the delegated avenue
```

**When it runs.** On every `spt adapter update` of this adapter, **and on
`spt adapter add`** (all three sources — install is the first update; since
v0.19.0): an eager-extract acquisition (`--release` / `gh_release`) runs it
right after registration; a `delegated` acquisition runs it only once the
acquisition command succeeded. The one exception is a `file_pull` add with no
payload yet (`ADAPTER_INSTALL_PENDING`) — nothing is installed, so no
post-step until the payload lands via the update engine. It runs
**unconditionally** on updates — even a no-op version check — so make the
step idempotent and let its own check decide what to do.

**Execution model — foreground, bounded, no background leg.** The step runs
as a child of the `spt adapter add`/`update` process, cwd = the adapter's
install dir, with a **120-second timeout** (a hung step is killed and counts
as failed). spt-core never backgrounds it and never detaches it: when the CLI
returns, the post-step has finished (or failed). If your step spawns and
detaches its own child, spt-core cannot see that child or its errors — keep
real work in the foreground and finish within the bound.

**stdin seam.** One JSON line (additive keys — ignore unknown):

```json
{"adapter_applied": true, "adapter_name": "spt", "profile_name": null,
 "version": "0.21.0", "previous_version": "0.20.0", "adapter_dir": "…"}
```

**stdout arbitrates the post-update notice** (exit code is orthogonal):
non-empty custom text **supersedes** the static `[update].message`
(markdown-rendered); the reserved sentinel `!!update-message!!` fires the
static message; empty prints nothing.

**Failure contract — how it surfaces.** A nonzero exit, spawn failure, or
timeout prints `ADAPTER_UPDATE_POST_FAIL:<adapter>: …` **with your step's
stderr detail, on the CLI's stderr**, and the CLI **exits nonzero**. The
committed pull/registration is never rolled back (failure-isolated), and —
deliberately — the static `[update].message` still fires when the adapter
applied: a post-step failure never swallows the adapter's own notice.

**Verify-then-notify (recommended).** Because the static message prints even
when the post-step failed, a static message that promises success ("finishing
in the background…") can read as a happy install over a failed one to an
operator watching only stdout. Instead: keep the static `[update].message`
modest (or omit it), have the post-step **verify its own work** and print a
custom success notice on stdout only when verified, exit nonzero when not —
and have whatever invokes `spt adapter add`/`update` check the **exit code**
and surface **stderr**. That combination makes a fresh-install failure loud
end to end.

## Shell adapters (`kind = "shell"`)

A shell adapter provides a **driven surface** (notifier, robot, sensor)
instead of hosting agents: same file, different body — the `[shell]` section
is required for (and exclusive to) `kind = "shell"`. See
[Shells: getting started](../shells/getting-started.md) for a worked,
shipping example; the field reference:

```toml
[shell]
spawn = "my-shell --link {link_token}"  # broker-launched; opaque template
ephemeral = false              # true -> no offline perch, no history retention
broadcast = "subnet"           # "subnet" | "same-node" | "none" (discovery scope)
command_receipt = "stdin"      # "http" | "stdin" | "relay" (how commands arrive)
pre_close = "park-and-save"    # optional instruction sent on link-break
close_timeout_ms = 3000        # graceful-termination window
persistent = true              # auto-online whenever the owner endpoint is online
wake_command = "my-waker --link {link_token}"  # offline wake-watcher; exit code 86 = wake
can_shutdown = false           # may the shell fire `api owner-shutdown`?
require_approval = "none"      # "none" | "remembered" | "always" (per-spawn gate)
max_instances_per_owner = 4    # optional cap (online + offline both count)
over_cap = "reject"            # "reject" | "approve" at the cap

[shell.capabilities]           # the agent->shell command vocabulary (durable)
notify = { args = ["title", "body"] }
clear  = {}

# A capability may carry its OWN approval gate (independent of the per-spawn
# gate), with an optional class_key scoping the grant finer than the verb:
[shell.capabilities.attach]
args = ["busid"]
require_approval = "remembered"  # "none" | "remembered" | "always" (per-act gate)
class_key = "hid"                # a remembered hid grant never authorizes another class

[shell.sensory]                # the shell->agent sensory vocabulary (live-only)
types = ["event"]

[shell.drive]                  # the owner->shell continuous control channel
types = ["stick"]              # latest-wins, ephemeral, never spooled (real-time input)

[shell.tunnel]                 # an opaque reliable-ordered byte stream pair (on-LAN)
enable = true
protocol = "usbip-urb"         # opaque label; the taxonomy never interprets the bytes
```

The capability, sensory, and drive vocabularies live in the manifest — spt-core
resolves them by adapter name, validates against them, and rejects anything
outside the declared vocabulary. The shell binary binds with
`spt api … bind-shell --link <token>` (the link token *is* the credential),
pushes sensory payloads with `spt api … emit`, and takes drive frames with
`spt api … drive-poll`.

Channel contracts differ — see [Shells: four channels](../shells/overview.md):
commands are **durable** (spooled, replayed); **drive** is **ephemeral**
(latest-wins, dropped if offline); **sensory** is **live-only**; the **tunnel**
carries **opaque bytes** the taxonomy never reinterprets (not enveloped, not
framed, not spooled — the link lifecycle closes it). The tunnel is reliable-
ordered ⇒ congestion is lag never loss ⇒ **on-LAN only**.

Per-capability `require_approval` reuses the same grant store as the per-spawn
gate; `class_key` narrows a grant to `(owner × verb × class × node)`. Shell
ownership is **owner-type-agnostic** — a Gateway (or any non-shell endpoint)
owns and drives a shell identically to an agent; exclusivity keys on the owner's
endpoint id, never its type.

## Cross-field rules (`spt adapter add` enforces these)

The schema validates structure; registration additionally enforces:

- `adapter.name` and `adapter.version` must be non-empty.
- `kind = "shell"` **requires** a `[shell]` section, which is **exclusive to**
  shell adapters (a `kind = "harness"` adapter omits it).
- `[history] strategy = "fetcher"` requires `fetcher`;
  `locate_normalize` requires both `locate_template` and `normalize_command`.
- `[digest]` requires a non-empty `extractor`. Under `strategy = "locate_normalize"`
  (the default) it **also** requires a resolvable source: either its own `source` or
  a `[history] locate_template` to fall back to — absent both, registration rejects
  (*"[digest] needs `source` (own-source) or a [history] `locate_template`"*). Under
  `strategy = "fetcher"` no `source` is needed (the extractor locates the transcript
  itself). The JSON schema alone accepts a bare `extractor`, so this only surfaces at
  `spt adapter add`.
- `[env.*] direction = "inject"` requires a `value`.
- `[update] avenue = "delegated"` requires `command`; `file_pull` requires
  `repo` **and** `signing_key`; `gh_release` requires `repo` (`asset` and
  `signing_key` optional).

A violation is a one-line error naming the field — fix and re-add.

===== /harness-contract/api.md =====

# The `spt api` surface

The imperative half of the harness contract: the inbound entry points a
harness's hooks (and a shell's binary) fire to keep spt-core's on-disk state
in sync. This page is the complete command reference plus the two startup
flows that tie it together.

Three rules apply to `api` calls:

1. **`--adapter <name[:profile]>` is an optional override** (since v0.9.0). For a
   harness-hosted session you normally **omit it**: `listen` resolves the owning
   adapter/profile at bind, from the seed's parent pid → the harness exe basename →
   the adapter(s) that declare it in [`[adapter] host_binaries`](manifest.md) → the
   active-profile pointer (set by [`spt adapter use`](../cli/reference.md)) or, with
   no pointer, the freshest-registered hosting adapter. Pass `--adapter` only to
   **pin** a specific adapter/profile (adapter dev, or explicit disambiguation).
   The profile qualifier `<adapter>:<profile>` is **runtime selection** — retained
   onto the perch record, and the daemon resolves the profile **overlay** when it
   later spawns the session's lifecycle roles. So a `live` profile whose
   `[session.psyche_init]` is in the resolved manifest is a **LiveAgent** (spt-core
   drives the Psyche as a bounded per-event turn); a profile without it is a **ReadyAgent**.
   Ready-vs-live is a profile choice, not a separate "go-live" verb.
2. **Prove association.** Commands that touch an existing perch take
   `--session-id <id>` (matching the perch's record) or a capability
   `--token`; shell commands authenticate with `--link <token>` (the link
   token minted at launch *is* the credential — no token, no access).
   This includes the read-side drain: an unauthenticated `api poll` is
   **refused** (exit 1, nothing printed) — see [`api poll`](#api-poll-id---include-deferred---link-token).
3. **Status rides stderr; stdout is payload; the exit code is authoritative.**
   Action-command status lines (`BOUND:<id> token=…`, `READY:<id>`, `SENT:`,
   `QUEUED:`, `WORKER_STARTED:…`, failure tags) print to **stderr** — always,
   piped or not (only the *color* is tty-gated; a redirect gets the bare tag
   unchanged). **stdout** is reserved for machine payloads: `--json` output,
   polled message frames, and documented payload emissions. A program
   shelling out must therefore capture **stderr** to read a status tag
   (`2>&1`, or capture the streams separately) — discarding stderr
   (`2>$null` / `2>/dev/null`) discards the status line by design — and
   should treat the **exit code** as the success contract: `0` = the action
   took effect, non-zero = it did not.

<!-- [doc->REQ-START-5] -->
```text
spt api [--adapter <name[:profile]>] [--manifest <path>] <command> …
```

`--manifest` points at the adapter's manifest for the commands that need it
(e.g. `capability`).

## The two startup flows

**Harness-hosted** — the harness owns the process; spt-core is invoked from
inside it (hooks):

```text
SessionStart hook ──► api seed --pid {parent_pid} --session-id {session_id}
session's listener ──► api listen <id>      (consumes the seed, holds the perch)
```

`seed` records an ephemeral hand-off keyed by parent pid; `listen` consumes
it, registers the perch, drains backlog, and blocks relaying events into the
session.

**spt-hosted** — spt-core spawns the session itself from the manifest's
`[session.self]` template, in its own terminal layer:

```text
spt-core spawns the template ──► session comes up
session (or its wrapper) ──► api bind <id> --set-session-id <discovered-id>
```

No seed file is involved; `bind` attaches the live session to its perch
post-spawn.

**Going ONLINE (the presence badge).** The `endpoint list` ONLINE badge means
one thing: a live process is **holding the relay** — an `api listen <id>`
that consumed a seed and is blocked relaying events. Binding alone does not
light it. A headless adapter binary (a gateway or any `[session.self]` host
that wants ONLINE presence and a live event stream) uses the same two-step
the harness-hosted flow does, against its **own** process:

```text
api seed --pid <own-pid> --session-id <sid>   (hand-off keyed to itself)
api listen <id>                               (consume, hold the perch, stay ONLINE)
```

Drop the listener and the endpoint decays to Dormant/Offline as its
last-seen ages out.

## Session lifecycle

### `api seed --pid <pid> --session-id <id>`

Harness-hosted startup, step 1: record an ephemeral seed keyed by the parent
process id. Fired by the harness's session-start hook. Prints `SEEDED:<pid>`.

<!-- [doc->REQ-LISTEN-SESSION-ID-FALLBACK] -->
**Seed lifetime.** The seed lives **in the daemon's memory only** — no file —
and survives until exactly one of: a successful `listen` bind consumes it, a
newer `seed` for the same pid overwrites it, or the daemon process restarts
(which drops the whole map). Nothing re-fires it until the harness's **next**
SessionStart. So an adapter must not rely on the seed for a session that goes
live late (hours after SessionStart) or after a daemon restart — that is what
`listen --session-id` (below) is for.

### `api listen <id> [--once] [--parent-pid <pid>] [--subnet <name>] [--session-id <sid>]`

Harness-hosted startup, step 2: consume the seed, register/hold the perch,
drain spooled backlog, then block relaying messages. `--once` runs a single
drain+receive cycle (testing). `--subnet` names the home subnet when this
creates a brand-new endpoint on a multi-subnet node (home is assigned
deterministically at creation).

<!-- [doc->REQ-LISTEN-SEED-CONSUME-AFTER-BIND] -->
**Recoverable refusals do not consume the seed.** The seed is consumed by a
**successful bind** — or by a refusal that proves the seed itself dead (see
spend-vs-restore below). A recoverable refusal that never bound —
`HOME_REFUSED` on a multi-subnet node without `--subnet`,
`ADAPTER_UNRESOLVED`, a live-perch conflict — leaves the seed consumable, so
the corrected retry on the same pid binds instead of dead-ending on
`NO_SEED`. (Effect before irreversible consume: the destructive step follows
the successful effect, never a recoverable refusal.)

**`--session-id <sid>` — binding when the seed is gone.** A session that goes
live late, or after a daemon restart, finds no live seed; with `--session-id`
the listener binds directly from the given harness session id (loud
`SID_BIND:<id>` marker). The fallback fires **only** on `NO_SEED` — every
other refusal keeps its own diagnostic — and carries the same identity/auth
gates as a seeded bind (live-conflict refusal, dead-anchor refusal on the
parent pid). Without a live seed **and** without `--session-id`, `listen`
refuses with `NO_SEED`.

> **Provenance caveat — same gates, weaker provenance.** A seed is a
> consume-once capability minted by the harness for exactly one anchor pid;
> `--session-id` is a **bearer string** — any local caller who knows a live
> session id can present it and revive that perch. Treat session ids as
> secrets: never log or publish them. (The local surface already trusts local
> callers — `--parent-pid` is an override — so this is a contract
> qualification for adapter authors, not a sandbox.)

**Refusals and the seed, spend vs restore.** A refusal that proves the seed
itself dead — a stale (dead-pid) anchor, an empty session id — **spends** it:
that seed can never retry as itself, and restoring it would re-arm a
dead-keyed seed for a recycled pid to steal. Recoverable refusals (the
`HOME_REFUSED` retry case above, a live-perch conflict) restore it.

### `api bind <id> [--set-session-id <sid>]`

spt-hosted startup: bind a freshly spawned session to its perch, recording the
session id discovered post-spawn. Identity precedes sessions — rebinding never
mints a new endpoint.

**Auth is intrinsic — `bind` takes no association proof.** It is an
*establishing* call (the exception to Rule 2), not a touch-an-existing-perch
call: spt-core spawned this session into its own broker-held terminal layer, so
that parentage *is* the credential. The only guard is ownership — an existing
*live* perch under a different session id is refused (you can only bind your
own). The broker injects **no** capability token into the spawned environment,
so there is nothing to echo back and no `[env.*]` entry to author for one; the
endpoint id arrives via the `{id}` fill in `[session.self]`, and that is the
only identity spt-core plants. `--set-session-id` *records* the discovered id
into the perch — it is not a proof.

`bind` prints `BOUND:<id> token=<token>`. The token is a freshly minted local
credential the session *may* retain for later authenticated calls, but it is
optional: every subsequent mutating call can instead prove association the
Rule 2 way, passing `--session-id <that same id>` for spt-core to match against
the record this bind wrote.

### `api boundary <clear|compact> <id> --to-session-id <new-sid> --session-id <prior-sid>`

<!-- [doc->REQ-BOUNDARY-ROTATION-CREDENTIAL] -->

The session was reset (context cleared or compacted) and continues under a new
session id: rebind the perch, preserving the endpoint's identity, spool, and
history across the boundary.

**The rotation catch-22 — read this before wiring the hook.** Rule 2 applies to
`boundary` like every mutating verb, but here the "matching" `--session-id` is
the sid on the perch record — i.e. the session being **departed**, not the one
you are rotating to. `--to-session-id` is the *payload*, never the *proof*. By
the time your rotation hook fires, the old session's context (its env, its
per-session files) is typically gone and the hook payload carries only the new
sid — so a hook that only knows "the current sid" cannot authenticate this one
verb. Two hard requirements for adapter authors:

1. **Persist the current session id at every SessionStart** in adapter-owned
   state keyed by the *endpoint id* (reference pattern:
   `{adapter_dir}/state/session/<endpoint_id>.sid`, single line, rewritten each
   SessionStart). At clear/compact, read it back as the **prior** sid and pass
   it as `--session-id`. Do **not** persist it in a per-session env file — that
   file dies with the session, which is the catch-22 itself.
2. **Never skip or swallow this call.** A rotation that is silently skipped
   (unresolvable endpoint id) or silently refused (`AUTH_REFUSED` on stderr,
   exit 1) leaves the perch pinned to the dead sid — after which **every**
   id-scoped call from the live session refuses, including a boundary retry,
   and the endpoint strands with zero message delivery until a full relaunch.
   Surface the id-resolution failure and the refusal reason loudly in your hook
   output. Resolve the endpoint id from your stable identity (`$SPT_ENDPOINT_ID`),
   not by looking up the new sid (it is not registered yet — the same catch-22).

A perch stranded by a *crashed* session (recorded pid dead) self-heals on the
next call via the dead-owner re-pin; a **live** session's rotation always
requires the prior-sid (or `--token`) proof. The long-term design (ADR-0032)
adds an OS-verified ancestry proof keyed on the endpoint's stable `parent_pid`
anchor, which will make the persisted-sid pattern optional; until then it is
required.

### `api psyche-download <id> [--session-id <sid>]`

Pull the agent's **resume context** to stdout, for a SessionStart hook to
inject as the session's additional context after a `/clear`, `/compact`, or
fresh resume. Emits the durable two-tier mind — the agent's role, its
cross-project live context, and the current project's context — **plus** any
commune/signoff drop that has been written but **not yet synthesized** into
that durable context (as `<pending-commune>` / `<pending-signoff>` slices), so
a just-written delta is never invisible on resume. The project is resolved
from the perch's recorded cwd. Read-only — it never writes the mind store.
Prints `NO-CONTEXT:<id>` on stderr (exit 0) when nothing is stored yet, the
adapter's fresh-init signal.

> The *read-back-in* half of the commune/signoff file-drops (the *write*
> side): the agent drops its delta, spt-core synthesizes it into the durable
> mind, and `psyche-download` is how the next session reads that mind back in.
> Wire it into your SessionStart hook alongside `seed`/`listen`, and inject
> its stdout — that is how a resumed session keeps its accumulated context.

### `api session-end <id> [--erase]`

Soft teardown: the session is over; the perch's spool and history are
preserved (that's what makes the next `poll`/`listen` drain work). `--erase`
hard-wipes instead — the exception, not the rule.

### `api shutdown <id>`

Graceful live-agent signoff: runs the final echo-commune **before** teardown
(the context delta is never lost to ordering), then soft-stops. This is what
the `spt endpoint shutdown` lifecycle path calls.

## Activity and presence

### `api state <busy|idle> <id> [--no-gate]`

Report the session's activity state. Activity/idleness comes from these
explicit reports — **never** from terminal quiescence, which lies. Reporting
`idle` also arms the echo gate (below) unless `--no-gate`.

### `api echo-gate <set|clear> <id>`

Manage the echo-gate sentinel directly. The gate marks "a summarization may
be needed when this session ends without a graceful signoff" — `state idle`
sets it as a side effect; a graceful signoff clears it.

### `api presence <id>`

Report user/agent presence at this endpoint (feeds most-recently-active
resolution across the subnet).

### `api driven-by <id>`

Print which node (if any) is currently remote-driving this endpoint, so a
session can tell whether input is local or remote.

## Messages

### `api poll <id> [--include-deferred] [--link <token>]`

Drain delivered messages over the hook channel (the pull-based path for
harnesses whose hooks can't inject). Deferred-flagged rows are excluded
unless `--include-deferred`. With `--link` this is the shell-flavored drain:
the link token authenticates, and the rows are the shell's stamped
command/text/file frames.

**Authentication is required** (rule 2 above): the drain must prove
association with `--session-id <sid>` (the perch's recorded session) or a
capability `--token` (`--link <token>` for the shell flavor). An
unauthenticated `poll` is refused with **exit 1 and no output** — messages
are addressed to the endpoint's occupant, not to whoever asks.

### `api history-log <id>`

Append normalized history (body on stdin) to the endpoint's native history
store — the push half of `[history] strategy = "native"`.

## Workers

Nested, short-lived agents under a parent endpoint. A worker is process-local
machinery — it authenticates with its parent's session id and carries no
capability token of its own.

<!-- [doc->REQ-WORKER-MINTED-NAME] -->
### `api worker-start <parent> [--agent-id <id>] [--agent-type <type>]`

Create a nested worker perch under `parent`. The worker id is **minted by
spt-core**, not supplied by the caller: `{parent}-w{N}` with a persistent,
per-parent counter. The caller does **not** pass an id (a stray positional id is
rejected).

Output channels follow the api status-line discipline:

- **stdout** carries the bare minted id and nothing else (the machine-readable
  result — empty on any refusal). Read this to learn the worker's id.
- **stderr** carries the human line `WORKER_STARTED:{parent}-w{N} under {parent}`.

`--agent-id` / `--agent-type` are optional: the caller's own agent identifiers,
recorded on the worker as **correlation metadata only** — never the perch
identity.

Authenticates against the **parent** (the parent's session id or token); the
worker record stores the parent's current session id as its registration sid.

<!-- [doc->REQ-WORKER-SID-SYMMETRIC-AUTH] -->
### `api worker-stop <id> --session-id <sid>` · `api worker-poll <id> --session-id <sid>`

Soft-stop (drop the ready marker; info + spool preserved) or drain a worker.
Both authenticate **symmetrically by session id** — no token. A presented sid is
accepted when it matches **either** the worker's stored registration sid **or**
the parent's *current* session id, so a context clear/compact that rotates the
parent's sid between start and stop does not lock the worker out. The natural
call `worker-stop <id> --session-id <parent sid>` is therefore correct as-is.

## Shells

The driven-surface flavor of the contract. The **link token** minted at
launch is the only credential a shell binary ever holds or needs:

### `api bind-shell --link <token>`

The shell binary's first call: resolve the instance **by link token alone**
(the spawn template carries only `{link_token}`; the owner is derived from
the link) and flip it online.

### `api emit <id> <payload> --type <type> --link <token>`

Push a sensory payload (one of the manifest's declared `[shell.sensory]`
types) to the owner's **live** session. REST-only by definition: never
spooled — if the owner isn't live, it's dropped with a diagnostic. Sensors
report the present, not the past.

### `api owner-shutdown <id> --link <token>`

A shell suspends its linked owner directly (e.g. a power-button surface),
bypassing agent messaging. Gated by the manifest's `can_shutdown`
pre-consent flag — fail-closed; an undeclared shell gets a refusal. The
firing shell cascades offline with its siblings, by design.

## Introspection

### `api capability`

Print the adapter's declared `hostable_types` (requires `--manifest`). The
cheap way to smoke-test that spt-core reads your manifest the way you meant
it.

### `spt whoami` — the identity verb *(identity-only since v0.33.0)*

<!-- [doc->REQ-WHOAMI-IDENTITY-ONLY] -->
The bounded-time "which endpoint am I?" answer for hooks and adapter glue:
resolves the calling session to its endpoint (`$OWL_SESSION_ID` /
`$SPT_AGENT_ID` / process ancestry) and prints that ONE endpoint's SELF line —
id, liveness, description. **The no-derivation bound is the contract**: whoami
never enumerates the roster, never derives projects, never runs git, never
touches the network — safe to call from deadline-bounded hook paths (the class
that previously timed out and black-holed message delivery). Unresolved is a
clean answer, not an error stall: `NO_PERCH` on stderr (`--json`:
`{"id": null}`), exit 1. `spt whoami --json` emits the committed identity
shape `{id, state, ready, alive, unbound, description}` — additive evolution
only. The full roster view lives on `spt endpoint list`; `api endpoint-info`
is NOT an identity carrier (it derives projects).

## Conventions

- **Output is line-oriented and stable**: `SEEDED:<pid>`, `READY:<id>`,
  `SENT:<id>`, `QUEUED:<id>`, error lines as `CODE:detail`. Parse lines, not
  prose.
- **Exit codes**: `0` success; non-zero = refused or failed, with the reason
  on stderr.
- **Commune/signoff are file-drops, not api commands.** An agent writes
  `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md` into the manifest's
  watched directory; spt-core's watcher ingests it. There is deliberately no
  `api commune`.

===== /harness-contract/echo-commune.md =====

# Echo-commune — the I/O contract

When a session ends **without** a graceful signoff, its context would be lost.
The **echo-commune** recovers it: spt-core runs the adapter's bounded
summarizer over the session, captures the brief the summarizer prints, and
files it as the session's context delta — the same delta a hand-written
[commune](../lifecycle/overview.md) would have carried.

This page is the **adapter-facing I/O contract** for that mechanism: the role
you declare, the keys spt-core fills, what it does (and does not) feed the
summarizer, how the summarizer locates the harness, and the drop-file
protocol spt-core uses to file the result. It is the companion to the
[`[session.echo_commune]` role](manifest.md#sessionrole--outbound-templates)
in the manifest reference.

> The echo-commune is spt-core's. The adapter supplies **one command template
> and one watched directory** — spt-core owns the spawn, the keys, the
> file-drop, and the ingest. Everything below is the seam between those halves.

<!-- [doc->REQ-DOC-ECHO-COMMUNE-CONTRACT] the full echo-commune I/O contract: the role + fields, the key catalog spt-core fills, the no-history-on-stdin rule, read-env self-locate, the single-writer/per-endpoint-resolver/ingest-deletes drop-file protocol, and stdout ingestion -->

## The role

`[session.echo_commune]` is one outbound role template. Its fields are the
standard role shape:

```toml
[session.echo_commune]
command = "my-harness run --agent summarize --session {session_id}"
recursion_guard_env = "SPT_ECHO_COMMUNE"     # set on the child so its own hooks bail
env_remove = ["MY_HARNESS_SESSION_ID"]        # stripped from the child's env
keys = ["id", "session_id"]                   # the keys this template expects filled
```

| Field | Required | Meaning |
|---|---|---|
| `command` | yes | Opaque command line with `{key}` placeholders. Model, tools, flags — all inside the string; spt-core never parses it. |
| `cwd` | no | Working directory for the child (substitutable). A role `cwd` wins over the endpoint default. |
| `recursion_guard_env` | no | Env var name set on the summarizer child so *its* harness hooks bail — no echo-of-an-echo. |
| `detach` | no (default `false`) | Spawn detached. |
| `env_remove` | no | Env vars stripped from the child's inherited environment. |
| `keys` | no | The substitution keys spt-core fills for this role (your declared expectation list). |

The child runs **bounded** — a timeout caps it, and a non-zero exit files
**nothing** (the failure is loud, never a half-written delta).

## Keys spt-core fills

For an echo-commune spawn spt-core fills its base catalog. Template only the
keys you are given; a `{placeholder}` spt-core does not supply for this role
fails the spawn with a one-line error naming the missing key.

| Key | spt-core fills it with |
|---|---|
| `{id}` | The endpoint id being summarized. Always filled. |
| `{session_id}` | The harness session id — filled when one is known. |
| `{node}` | This node's advertised label — filled only when non-empty (a `{node}` reference with no value fails loud rather than resolving an empty token). |
| `{subnet}` | The endpoint's home-subnet label (`local` when unhomed) — filled only when non-empty. |
| `{adapter_dir}` | The adapter's install dir — adapter-static, always available (lets the command point at the adapter's own packed summarizer binary). |
| `{adapter_name}` | The adapter's declared `name`. |
| `{VAR}` | Any manifest-declared [`[env] direction = "read"`](manifest.md#envvar--env-var-table) var captured at bind — see [Self-locating the harness](#self-locating-the-harness). |

This is the **same base catalog** the Psyche and notification roles build on;
it deliberately does **not** include `{session_name}` (a `[session.self]` key)
or the Psyche-only `{psyche_context_file}` / `{parent_session_id}`.

## History is not fed on stdin

spt-core has a stdin channel for the summarizer — but **you must not depend on
it carrying the transcript.** The rule, field-proven against the reference
Claude Code adapter:

- If the manifest declares a [`[history]`](manifest.md#history--transcript-access)
  strategy that yields records, spt-core normalizes them and pipes them to the
  summarizer on **stdin**.
- With **no `[history]` section**, or a `native` history store that is still
  empty, there are no records — so **stdin is empty**. This is the field case:
  the reference adapter's history is native and typically empty at echo time,
  so the summarizer receives an empty stdin.

The load-bearing consequence for an adapter author: **the echo-commune command
must self-source the session it summarizes** (locate and read the transcript
itself), exactly as a [`[digest]` `fetcher`](manifest.md#digest--session-digest-extractor)
extractor does. Do not write a summarizer that reads its transcript from
stdin. What spt-core reliably supplies is the **command template with the key
catalog filled** (and the child's env — see below); the transcript is the
summarizer's to find.

## Self-locating the harness

A summarizer that self-sources its transcript needs to find the harness's
config/log root. spt-core carries that in through the manifest's **read-env
allowlist**, so the value survives the daemon boundary (the echo child runs in
the daemon context, where the original launch environment is long gone).

```toml
[env.CLAUDE_CONFIG_DIR]
direction = "read"
value = "~/.claude"          # fallback when the launch env didn't set it
```

- Declare each locator var with `direction = "read"`. spt-core captures it
  **from the launch environment at bind** — an explicit allowlist, never the
  whole environment, and only when the ambient value is actually present.
- The captured value is written onto the perch record, so it is available when
  the echo (or digest, or Psyche) child spawns later.
- At spawn spt-core injects it as a `{VAR}` substitution key. **Resolution
  order:** the captured ambient value wins (a relocating profile — e.g. a
  wrapper that sets `CLAUDE_CONFIG_DIR` to a private dir) → else the
  directive's own `value` fallback (the base harness default, e.g.
  `~/.claude`) → else the var is **omitted**, so a `{VAR}` reference fails loud
  rather than resolving a wrong path. A leading `~` expands to the home dir.

Reference the captured key in the echo command the same way `[digest]` does:

```toml
[session.echo_commune]
command = "my-harness-summarize --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}"
keys = ["session_id", "CLAUDE_CONFIG_DIR"]
```

## Drop-file protocol

spt-core files the summarizer's output as a **drop file** — the exact same
channel a hand-written commune uses — then ingests and deletes it. Three
invariants define the contract:

**1. spt-core is the single writer.** The filename is fixed by spt-core —
`<endpoint_id>-commune.md` — and the adapter declares only the *directory*
(`[session] commune_dir`). The mind never writes this file; spt-core writes it
atomically (with a bounded access-denied-only retry for transient
antivirus/indexer locks) and is the sole deleter. Never have the summarizer
write the drop file itself.

**2. The directory resolves per-endpoint** *(hardened in v0.29.0).* A
`commune_dir` may be absolute or relative:

- **Absolute** → used as-is.
- **Relative** → resolved against the **endpoint's own recorded working
  directory**, read fresh at drop time — never against the daemon's process
  cwd.
- **Relative with no recorded cwd** → spt-core **skips the drop and warns once**
  (per endpoint, per daemon run). It never guesses and never falls back to the
  daemon's cwd. This is the fix for a real outage: under a service-launched
  daemon whose process cwd was a system directory, a relative drop dir once
  resolved there and failed with a permission error on every write. The loud
  skip makes a mis-declared relative dir a diagnosable signal instead of a
  silent failure. Declare an **absolute** `commune_dir`, or ensure the
  endpoint's cwd is recorded, to avoid the skip.

A missing `[session.echo_commune]` role is likewise a **loud once-skip**, not a
retried fault.

**3. Ingest deletes the drop.** On its next pulse tick spt-core reads the
drop, routes it into the durable context tiers, and **deletes the file** —
whether the content was written or suppressed as a stale snapshot (both mean
"consumed"). A read/write *error* leaves the file in place to retry on the next
pass. The file disappearing is the success signal.

## What spt-core expects on stdout

The summarizer's **stdout is the brief** — the cheap-model synthesis of the
session, as plain text. spt-core does not require a structured format at write
time; it stamps a provenance header (`Source: echo-commune`) and writes the
result as the commune drop file.

On the later ingest tick that body is parsed with the **two-slice envelope**
grammar, the same one a hand-written commune uses:

- `<live-context>…</live-context>` → the **live tier** (who the agent is and
  what it is doing; follows the endpoint everywhere).
- `<project-context>…</project-context>` → the **project tier** (scoped to the
  current project).
- An **untagged body** routes whole to the live tier.

Every write is precedence-guarded — a stale snapshot arriving inside another
writer's protection window is suppressed (but still consumed and deleted). The
checkpoint sentinel `!!checkpoint!!`, if the brief carries one, is stripped
before both presentation and the durable write, so it never persists in the
stored context.

## In one line

Declare `[session.echo_commune]` with a command that **self-sources its
transcript** (found via a `direction = "read"` locator key) and **prints the
brief to stdout**; declare an **absolute** `commune_dir`; let spt-core do the
spawn, the file-drop, the ingest, and the delete. That is the whole contract.

===== /harness-contract/install-on-demand.md =====

# Install-on-demand bootstrap

<!-- [doc->REQ-INSTALL-BOOTSTRAP-VERB] -->
How an adapter ships spt-core *with itself*. The contract: **the canonical
install path is also every adapter's pack-in installer** — there is no second
mechanism, no vendored binary, no bespoke fetch logic to maintain. Your
adapter checks for `spt`, and when it's missing runs the official bootstrap:
`gh release download` the platform binary from the release channel, then the
binary's own **`spt install` verb** — it places itself at the canonical
install path, registers the user PATH, refuses a wrong-platform binary, and
is non-interactive and idempotent by construction.

The release channel is private, so the node needs an authenticated GitHub CLI
(`gh auth login`, once per machine, with an account that can read the
channel) — see [Installing](../reference/install.md).

## The generic contract

```text
if `spt` is on PATH        -> done (optionally check `spt --version` ≥ your floor)
else                       -> gh release download the platform binary + run its `install` verb
then                       -> first invocation may need the absolute path (Windows)
then                       -> register your manifest: spt adapter add --github <org>/<repo>
```

After first install, spt-core keeps itself current (signed self-update over
the same gh channel), so the bootstrap can leave upgrades to spt-core. The
remaining bootstrap step is to register your adapter — see
[Activate the adapter](#activate-the-adapter--register-your-manifest) below.

## Check-and-install: POSIX sh

Drop this into your adapter's bootstrap (plugin install step, postinstall
script, first-run guard):

```sh
if ! command -v spt >/dev/null 2>&1; then
  echo "spt-core not found - installing..."
  gh release download --repo BigscreenVR/spt-bs-releases \
    --pattern 'spt-x86_64-linux' --dir /tmp
  chmod +x /tmp/spt-x86_64-linux
  /tmp/spt-x86_64-linux install
  # current shell may not see the PATH update yet:
  SPT="${SPT_HOME:-$HOME/.spt-core}/bin/spt"
else
  SPT="spt"
fi
"$SPT" --version
```

## Check-and-install: PowerShell

```powershell
if (-not (Get-Command spt -ErrorAction SilentlyContinue)) {
    Write-Output "spt-core not found - installing..."
    gh release download --repo BigscreenVR/spt-bs-releases `
      --pattern 'spt-x86_64-windows.exe' --dir $env:TEMP
    & (Join-Path $env:TEMP 'spt-x86_64-windows.exe') install
    # The user-PATH registration only reaches NEW terminals -- use the
    # absolute install path for everything in THIS process:
    $spt = Join-Path $env:LOCALAPPDATA 'spt-core\bin\spt.exe'
} else {
    $spt = 'spt'
}
& $spt --version
```

## Activate the adapter — register your manifest

Installing the binary is the first half of a pack-in; registering your manifest
is the second. Installing the binary makes `spt` available; **`spt adapter add`
activates your adapter** — registration is what lights up its profiles,
`[strings]` bodies, `[digest]` extractor, and hooks and makes it show in
`spt adapter list`. So the step right after the binary check is registering the
manifest:

```sh
# after `spt` is confirmed present (above):
# from a GitHub release — ships built binaries, source-free, versioned:
"$SPT" adapter add --release <your-org>/<your-adapter-repo>               # latest
"$SPT" adapter add --release <your-org>/<your-adapter-repo> --tag v1.0.0  # pinned
# ...or clone a repo whose ROOT holds manifest.toml:
"$SPT" adapter add --github <your-org>/<your-adapter-repo>
# ...or a local directory your harness ships:
"$SPT" adapter add ./adapter
```

`adapter add` is **manifest-first** — a clean add proves the cross-field manifest
shape — and it conducts your
[`[update]`](manifest.md#update--adapter-self-update) avenue once (install is the
first update). Confirm with `spt adapter list`: your adapter and its version
appear. Keep this idempotent in your bootstrap the same way the binary check is —
register when `adapter list` shows your adapter missing or below the expected
version.

**`--release` is the recommended distribution.** It fetches a `.spt` archive
asset — a tar whose root holds `manifest.toml` + `strings/` + the binaries the
manifest points at — from the named GitHub release, extracts it to the durable
registry home, and registers the root. That ships your **built binaries**,
source-free and **versioned by tag** (`--tag`, default the latest release), and
first-acquisition trusts gh's authenticated TLS + GitHub exactly like the
bootstrap's first binary fetch. A development **monorepo stays a monorepo**: your
release CI packs the archive (`tar -czf adapter.spt manifest.toml strings/ bin/…`)
and uploads it as a release asset, so the adapter ships straight from your
existing repo. Override the asset name with `--asset` (default `adapter.spt`).

**Cover several platforms in one `.spt` (since v0.13.2).** To ship binaries for
more than one OS/arch in a single asset, add a **target-triple subdirectory** at
the archive root per platform and put that platform's binaries inside it, leaving
the shared `manifest.toml` + `strings/` at the root:

```text
adapter.spt
├── manifest.toml                # shared — at the root
├── strings/                     # shared — at the root
├── x86_64-pc-windows-msvc/      # one platform's binaries…
│   └── bin/…                    #   …in the same relative layout a flat .spt uses
└── x86_64-unknown-linux-gnu/
    └── bin/…
```

On install, spt-core extracts the shared root plus **only the current node's
triple**, flattened into the install dir — so the bare-name
`<install_dir>/<program>` resolution above is unchanged; mirror, under each
triple, exactly the per-platform tree a flat `.spt` would place at the root. The
recognized triples are `x86_64-pc-windows-msvc` and `x86_64-unknown-linux-gnu`; a
root subdirectory whose name is **not** a recognized triple is treated as a shared
root entry (so binaries for other platforms still ship as **separate
single-platform assets**, one selected per node with `--asset`). A multi-platform
archive that lacks the recipient's triple is refused with a clear
`NoArtifactForPlatform` error — never a silent partial install — and requires
`min_spt_core_version >= 0.13.2`. A flat archive (no triple subdirectories)
installs exactly as before.

`--github` is the alternative for an adapter whose **repo root already holds
`manifest.toml`**: it clones the repo and registers the clone root (`adapter add`
resolves a directory source to `<dir>/manifest.toml` at the root). Local
development uses the directory form, which takes any path or filename:
`spt adapter add ./adapters/my-adapter.toml`.

What registration holds under `adapters/<name>/` follows your
[`[update]`](manifest.md#update--adapter-self-update) avenue: a `delegated` or
`gh_release` adapter is **pointer-mode** (the manifest and `strings/` are read
live from the durable home), and a `file_pull` (or avenue-less) adapter is
**copy-mode** (the `manifest.toml` and `strings/` are copied in). Publish the
binaries your manifest references in the `.spt` (or repo) too, and reference them
**by bare name**: since v0.8.0, a command template's program token resolves
against the adapter's install dir before `PATH`, so a `.spt` that ships its
binaries is **self-contained** — the shipped binary is found without any PATH
placement. (Absolute paths still work; an unshipped tool still falls back to
`PATH`.) This applies to the `[session.psyche_resume]` per-event turn, the
`[digest]` extractor, and `spt adapter digest-proof`.

> **"Install the plugin, get the adapter for free" — include the activation
> step.** The [`[update]`](manifest.md#update--adapter-self-update) avenues
> keep a *registered* adapter current. The straightforward path for a
> `--release`-distributed adapter is **`gh_release`** (since v0.8.0): declare
> `avenue = "gh_release", repo = "your-org/your-adapter"` and
> `spt adapter update` ships the latest release `.spt` to the node — fetched,
> optionally verified against your `signing_key`, re-extracted, and
> re-registered. The other avenues: `delegated` (your harness's own updater
> installs the content — set `self_verifies = true` to attest it verifies what
> it installs), and `file_pull` (its automatic network-pull transport is **on
> the roadmap**). Deliver the manifest with `adapter add --release` (or
> `--github`, or a packed local dir) and let `gh_release` carry updates.

## The Windows PATH-refresh gotcha

The install verb registers the binary directory on the **user** PATH via the
registry. Registry PATH changes reach **new** processes; an already-running
process — including the terminal (and *your bootstrap*) that just ran the
install — keeps the PATH it started with.

So: **the first invocation after an install must use the absolute path**
(`%LOCALAPPDATA%\spt-core\bin\spt.exe`; the verb prints it). Every new
terminal after that finds `spt` normally. The snippets above bake this in.
On Linux the equivalent (a `~/.profile` entry the current shell hasn't
sourced) is handled the same way: the printed absolute path, once.

## Pinning and install-dir overrides

Pin a release by giving `gh release download` an explicit tag
(`gh release download v0.32.0 --repo … --pattern …`). The verb's knobs:

| Flag | Meaning |
|---|---|
| `--dir <path>` | Override the install directory |
| `--no-path` | Skip user-PATH registration |

## Trust model

First fetch trusts gh's authenticated TLS + the private channel's access
control (check the download against the release's `SHA256SUMS` for a belt on
the braces). From then on, `spt update` performs full Ed25519 signature
verification against the two-key trust anchor embedded in every binary — so
the bootstrap is the strong link only once.

===== /harness-contract/patterns.md =====

# Adapter patterns & pitfalls

The [integration checklist](integration-checklist.md) tells you *which* surfaces
to wire. This page is the field guide: the patterns that decide whether an
adapter merely registers or runs like a native part of the harness — the design
rules, the lessons that save you a debugging session, and the cheapest ways to
prove each piece on the live binary.

Everything here is behaviour of the **shipped public surface** — the `spt`
binary, the [manifest](manifest.md), and the [`spt api`](api.md) commands,
verified against a live binary. It is harness-agnostic; where one harness's quirk
is the clearest illustration it is called out as such, and the pattern
generalizes to any harness with the same shape.

## The one rule: manifests are static, logic lives in binaries

If you internalize a single thing, make it this.

- **Manifest fields are static templates spt-core fills.** A field is a fixed
  template: spt-core substitutes `{key}` placeholders from a fixed
  [catalog](manifest.md#substitution-keys) (`{session_id}`, `{parent_pid}`,
  `{adapter_name}`, `{id}`, the digest/psyche keys), and `~` expands to home.
  That is the whole of a template's power.
- **Anything that depends on runtime state belongs in a binary the manifest
  points at** — the `[digest]` extractor, a `[session.*]` runner. Reading an env
  var, branching on runtime state, or computing a value is *logic*, so it lives
  in a binary. If your harness can move its own state directory at runtime, for
  example, treat the manifest `source` as a *fallback* root and have the binary
  it points at resolve the real location itself.
- **A `.toml`-only leaf carries no code of its own**, so verify it by registering
  and resolving it on the live binary (below), and put anything you want covered
  by real tests into a binary (an extractor or runner).

Hold this rule and most of the surface falls into place: the manifest is the
*declaration*, your binaries are the *behaviour*.

## The adapter lives in the registry

An adapter — its manifest, profiles, `[strings]`, the `[digest]` extractor, any
runner binaries — is registered with **`spt adapter add <dir>`** into the
node-local adapter registry. The version recorded there (`spt adapter list`) is
the **version-of-truth** for what the adapter does. That is the entire, universal
delivery mechanism: every spt adapter ships this way, and registration is where
spt-core validates it (see [the second gate](#validate-against-the-live-binary)).

If your harness *also* has a plugin or marketplace channel (so casual users can
one-click install it), that is a separate distribution choice on top. When you go
that route, let the **registry** carry the binary, manifest, and runtime state,
and version the plugin independently of the manifest/binary.

## Profiles are sparse leaf-replace overlays

A profile is selected as the composite `<adapter>:<profile>` and
**leaf-replaces only the leaves you declare** — everything else inherits from
base. Override exactly what differs:

- `[profiles.<name>.session.self].command` — retarget the bringup command (for
  example, wrap the launch in another binary).
- `[profiles.<name>.digest].<key>` — widen one digest knob.
- `[profiles.<name>.session.psyche_init]` — add the [live-agent
  seam](#the-live-agent-companion-seam); its presence on the merged view is what
  flips an endpoint to a live agent.

**Make an overlay observable.** Also leaf-replace one `[strings]` key (say a
label) in the profile. Then `spt adapter get-string <adapter>:<profile> <key>`
differs from the base value — and that diff is your proof the overlay resolved.
It is the cheapest profile acceptance assertion there is.

A profile that wraps the launch in another binary works when that binary is a
drop-in for the base harness binary on the same argv and passes inherited env
through unchanged. Routing a session through a launcher wrapper (a model or
billing multiplexer, say) is exactly this: replace the `session.self` command and
let the injected endpoint-id env ride through untouched.

## Wiring hooks: you own the harness side

spt-core supplies the harness-**independent** `spt api` primitives and their I/O
format. *You* author all harness-specific wiring: spt-core supplies the
primitives, and your adapter hand-writes its hook config to shell out to
`spt api`. A mapping that works on the public surface, in terms any harness can
translate to its own events:

| When the harness… | …fire | Why |
| --- | --- | --- |
| starts a session | `api seed --pid {parent_pid} --session-id {session_id}` | Seed the endpoint (adapter-agnostic) — keep this fast and non-blocking. |
| submits a user turn | `api poll {session_id}` | Drain the inbox to stdout (plus any keyword hints). |
| goes idle / busy | `api state idle` / `api state busy` | Honest activity; spt-core treats your explicit `api state` calls as the source of truth. |
| ends the session | `api session-end {session_id}` (or `api shutdown <id>` for graceful signoff) | Teardown that preserves the spool + history. |
| spawns / ends a sub-agent | `api worker-start` / `api worker-stop` | Nested short-lived workers. |

Two structural rules sit under that table:

- **Run the blocking listen/poll loop from a skill the user invokes.** Seed on
  start so bringup stays fast, and let an explicit `/ready`-style skill own the
  blocking stream.
- **Message delivery is stdout framing.** `api poll` emits the self-delimiting
  envelope `<EVENT type="msg" from="<sender>">body</EVENT>` (the live listener
  stream uses the same shape). Multi-message drains split cleanly on `</EVENT>`.
  Decode a body by splitting on `<br>` → newline, then HTML-unescaping
  `&lt; &gt; &quot;` and `&amp;` **last** (the full entity set and decode-order
  contract: [the `<EVENT>` wire contract](../messaging/overview.md#the-event-wire-contract)).
  Route that stdout into your harness's injection channel — that routing is
  adapter glue.

### Get these right in the hook layer

A few patterns here save you a debugging session — wire them deliberately.

- **Pre-empt an injection channel's size cap.** If your harness caps the size of
  an injected blob (truncating it, or spilling it to a file and evicting it from
  the context the agent actually reads), cap the combined hook output
  adapter-side: under the limit, pass the output through verbatim; over it, spill
  the **full** text to an agent-readable file and inject a short pointer. Always
  cut on an `</EVENT>` boundary, so every envelope stays whole and every message
  survives a large drain.
- **Inject a skill body *before* the perch gate, and gate only the message
  drain.** When the same prompt hook both injects a requested skill's
  instructions and drains messages, run the skill-body injection first — that
  keeps skills like "who am I" or "set me up" working for a new user, since they
  are valid while the perch is still being readied. Match the skill token as a
  leading token, so only an actual invocation fires (prose that merely mentions
  it stays inert).
- **Make the setup/installer skill self-contained in its stub.** It runs
  precisely when the binary may be absent (installing it is the job), so carry
  its operative steps in the harness-native stub itself — the floor that always
  works — and let any file-backed body mirror them for the binary-present repair
  path. The one skill that most needs delivery is the one delivery reaches last,
  so give it a stub that stands alone.
- **Read hook inputs from stdin.** A hook receives its data (the prompt, the
  session id) as a JSON object on **stdin** — parse that, which keeps a
  `/`-leading value (a `/<skill>` token, an absolute path) intact. Under
  Git-Bash/MSYS on Windows, an argument beginning with `/` is rewritten to a
  Windows path before your command sees it (a `/foo:send` token can arrive as
  `C:/Program Files/Git/send`), so stdin is the transport that preserves it. If a
  command must take such an argument, guard it (`MSYS_NO_PATHCONV=1`, or a
  file/stdin transport). It is the same class as the UTF-8-stdout trap below —
  choose the transport that carries the data faithfully.

## `[strings]`: keep the manifest thin, point at the live binary

A `[strings]` value is either an inline string or a **file pointer**
(`key = { file = "relative/path" }`), resolved lazily by `spt adapter get-string`
to the file's contents — so live edits reflect without re-registering. Keep
pointer files inside the `strings/` dir; the add enforces that containment. Use
file pointers to keep skill-instruction bodies out of the manifest.

When a skill body needs to describe the spt surface, point it at the binary's own
self-documentation, so the guidance stays current with the shipped binary — two
always-current tiers:

- `spt how-to <topic>` is the task-oriented agent-guidance surface, covering
  **selected** topics, each a canonical write-up of verbs, flags, and result
  codes. Treat it as the curated tier: for a verb it covers, read the topic; for
  any other verb, probe and fall through (an undocumented topic returns
  `NO_SUCH_TOPIC:<topic>`).
- For any verb, **`spt <verb> --help` is the always-present source-of-truth** —
  it tracks the shipped binary. A skill body that says "the verb list is
  `spt <noun> --help` — match the user's intent to a verb" stays correct across
  releases.

Either tier stays current with the binary, and a skill body that points at them
stays correct across releases. They are also the fastest way to learn the surface
while authoring — it self-documents.

## `[digest]`: the transcript→record extractor

The `[digest]` seam maps your harness's native transcript into spt-core's
digest-record contract. The contract beyond the schema:

- **Name where it reads** — either `source` or a `[history].locate_template`.
  `spt adapter add` requires this even though the JSON schema alone would accept a
  bare `extractor`; the cross-field rule surfaces at registration, so validate
  against the live binary.
- **Treat `--in {source}` as a root.** The extractor is invoked
  `--session {session_id} --in {source}` and locates `<session_id>`'s transcript
  *within* that root — your harness's internal subdir scheme is yours to resolve,
  and spt-core keeps the key catalog harness-agnostic. Handle both shapes: `--in`
  a directory (locate the session) and `--in` a direct file (the
  `digest-proof --sample` path).
- **Resolve a runtime-relocated state tree in the binary.** When a runtime value
  (an env var, an isolated profile) moves the real transcript tree, have the
  extractor prefer that value on its directory branch, with the manifest `source`
  as the fallback root. That resolution is logic, so it lives in the binary — the
  headline rule in miniature.
- **Emit raw records as UTF-8.** Output one NDJSON line per record
  (`{role ∈ input|agent|tool, text?, tool?, ts?}`) and leave presentation to
  spt-core's renderer (`window_turns`, arg truncation, sprint collapse). Pin
  stdout to **UTF-8** so non-ASCII (em-dashes, smart quotes) round-trips — spt-core
  reads the stream as UTF-8. (Native-UTF-8 languages get this for free, which is
  part of why this seam is a binary.)

Prove the whole path with `spt adapter digest-proof <adapter> --sample <file>`
(below).

## The bringup / launcher seam

`[session.self].command` is the spt-hosted bringup template — spt-core spawns it
into a broker PTY. For a harness with no native session-id flag, mint the id
internally and pass the endpoint id via an injected env var
(`[env.<VAR>]` with `direction = "inject"`, `value = "{id}"`); the start hook
reads that env and self-registers with `api bind <id>`.

That bind is **intrinsically authenticated**: for a broker-spawned session the
broker parentage is the proof, so `api bind <id> --set-session-id <discovered>`
alone establishes the association, and later mutating calls prove themselves with
the session id the bind recorded. (The flip side shows up in
[testing](#testing-against-a-real-harness-isolate-identity): the framework keys
association on *identity*, so identity is the thing you isolate.)

`adapter.shortcut_basename` brands the generated launcher shortcut
(`<basename>-<id>`) and is decoupled from the adapter name.

## The live-agent (companion) seam

An endpoint is a **live agent** exactly when its *resolved* manifest declares
`[session.psyche_init]` — declaring that section is the single go-live signal. A
base manifest is a ready agent; a profile overlay that adds the section makes a
live agent. spt-core checks this on the **merged** view, so the profile resolved at
**bind time** drives the spawn decision all the way through — the bound profile
governs the full runtime lifecycle, beyond bringup argv. Since v0.9.0 the seed is
adapter-agnostic: the profile is resolved when `listen` binds, from the
active-profile pointer ([`spt adapter use <adapter>:<profile>`](../cli/reference.md))
or an explicit `--adapter <adapter>:<profile>` override on the `listen` call.

- **`[session.psyche_init]` is a go-live GATE ONLY — spt-core never spawns it.**
  Its mere presence promotes the endpoint to a LiveAgent; the command is opaque
  and, in the per-event model, unexecuted. Keep it minimal.
- **The daemon drives the Psyche as one bounded `[session.psyche_resume]` turn
  per event** (a pulse fire, a commune/signoff drop, a session-custody
  transition) — stdin-fed and **stdout-captured**, exiting at turn end. There is
  **no resident wrapper, no seed-once, no detached process**. `psyche_resume` is
  the *sole* driven Psyche role.
- **Keys spt-core fills into the turn:** `{session_id}` = the Psyche's **own**
  custody sid (its own thread — a parent `/clear`/`/compact` does not rotate it),
  `{parent_session_id}` = the parent's sid, `{psyche_context_file}` = the path to
  the composed-mind file (fresh = non-empty, continue = 0-byte — never on the
  argv), and `{subnet}` (when known). The adapter-static/node keys are also
  available: `{id}` = the **parent endpoint id** (not a `<parent>-psyche`
  override), `{adapter_dir}`, `{adapter_name}`, `{node}`. There is **no**
  `{psyche_dir}` or `{psyche_prompt}` fill — those retired with the resident spawn.
- **The runner is yours to build; its lifecycle is the daemon's.**
  `psyche_resume.command` is adapter-authored and opaque to spt-core. **If your
  harness's headless mode runs one turn per invocation, that is exactly the
  model** — the daemon invokes `psyche_resume` once per event, so no resident
  wrapper is needed (and none is wanted). Build it like the `[digest]` extractor —
  a compiled, dependency-light binary the daemon can exec bare on any platform,
  resuming the Psyche's own session by `{session_id}` and reading the mind from
  `{psyche_context_file}`.

### Prove live bringup non-interactively

To prove your live path actually goes live — without an interactive terminal —
drive the bringup as a child process and assert on deterministic side-effects.
The harness plays the long-running-listener role:

1. **Seed**, anchoring on the OS process pid — not a shell-wrapper pid. Under
   Git-Bash/MSYS `$$` is the MSYS pid, which fails the seed's liveness guard, so
   derive the real OS pid: `spt api seed --pid <os-pid> --session-id <sid>`
   (adapter-agnostic — no `--adapter`).
2. **Bind, then send a probe.** A send to a never-bound perch is `NO_PERCH`
   (no spool exists yet), so establish the perch first; then `spt send <id>
   <probe>` `QUEUED`s against it, ready to drain on bringup.
3. **Spawn the persistent relay as a child**, capturing its stdout/stderr:
   `spt api listen <id>` (no `--once` — that exits after one delivery). The adapter
   resolves from your `[adapter] host_binaries`; pass `--adapter <a> --manifest <m>`
   only to pin a specific adapter/profile. Assert `BOUND:<id>` then `READY:<id>` on
   its stderr, and the relayed `<EVENT>` carrying your probe on its stdout.
4. **Assert the endpoint went live and its turns succeed.** The relay marks the
   perch online; the endpoint reports kind `live_agent` (its resolved manifest
   declares `[session.psyche_init]`). Because the Psyche is a **per-event turn,
   not a resident process**, there is **no `<id>-psyche` perch to come online**
   and **no `LIVEHOST_PSYCHE` marker** to assert — those belonged to the retired
   resident model. Instead assert the healthy-turn signal: after a psyche event
   fires, the endpoint's perch carries **no `psyche_host_error`** (an absent
   error is the "turns succeed" proof; a present one names the failing turn).
5. **Kill the child** to end the session — the relay is freely killable; the
   Psyche runs only as bounded per-event turns the daemon drives (a graceful
   `spt endpoint shutdown <id>` ends the endpoint).

Pin the identity env (`OWL_SESSION_ID`) for the auth-gated calls, and give the
system-under-test a throwaway identity per the
[identity-isolation rule](#testing-against-a-real-harness-isolate-identity).

## Lifecycle continuity is file-drops

Commune and signoff are delivered as **file-drops** by design. The agent writes
`<endpoint_id>-commune.md` (delta context) or `<endpoint_id>-signoff.md` (final
save) into the manifest-declared `[session].commune_dir` / `signoff_dir`;
spt-core's daemon watcher ingests it and deletes it (the daemon is the single
writer). The **filenames are contract-fixed** and the directory is
adapter-declared, so wire the directory watch and read the contract filename.
This is the single biggest continuity win, so it is worth getting exactly right.

## Testing against a real harness: isolate identity

The surest way to prove your hook wiring fires is an acceptance test that
**spawns a real harness session as the system-under-test**. Doing so meets a
framework property you design around:

- A perch's identity is **resolved from the environment** (the same vars
  `spt whoami` reads), and perches are **name-keyed, last-establish-wins**. The
  most recent session to establish a perch under a given identity holds it,
  taking the active poll/listen stream with it.
- So a spawned test session that loads your adapter (whose start hook seeds and
  binds a perch) under the identity of the agent running the tests would take
  that agent's perch. **Identity isolation is the guard.**
- Give every spawned system-under-test a disposable identity distinct from any
  live agent — override **both** identity env vars before the spawn to a
  throwaway `<adapter>-ci-<n>`, so the nested session and the operator's perch
  coexist cleanly. Identity is the key, so isolating identity is the whole guard.
- Keep the orchestration deterministic and **assert on a hook side-effect** — a
  marker or digest file, or `spt` state — the deterministic signal. Keep the
  harness as the system-under-test and let its side effects be your assertions.

## Validate against the live binary

Treat registration as the **second gate**: beyond JSON-schema validity,
`spt adapter add` runs cross-field checks that go past what the schema expresses
(the `[digest]` source rule is one). Build for it:

- A **registration integration check**: `adapter add` → `adapter list` (assert
  the adapter and each shipped profile composite resolves) → `get-string` (the
  base value, each overlay diff, and each file-backed pointer resolve to a body)
  → a soft `adapter remove` (leaving the registry clean). Gate it behind an
  opt-in env flag and a minimum `spt` version, since it mutates the node-local
  registry.
- Two author-time tools work without a live session:
  - `spt api --adapter <a> --manifest <file> capability` reports the manifest's
    hostable types from the manifest alone — assert it advertises the type your
    bringup spawns. (A clean `add` already proves the cross-field shape, since add
    is manifest-first; `capability` is the lighter, non-mutating check.)
  - `spt adapter digest-proof <a> --sample <file>` runs the real extractor
    through the registry and renders the result — proving the
    transcript → record → render path end-to-end on a fixed sample. It fills the
    same runtime substitution keys the daemon does, so passing proof means it
    works at runtime. (Use a recent `spt` — current binaries fill the full key
    map.)
  - `spt adapter translate-proof <a> --event '<EVENT…>'` spawns and feeds your
    declared `[message-idle-translation-binary]` exactly as the daemon does at
    idle delivery, then prints the keystroke-command stream it emits
    (`{key}` / `{text}` / `{delay_ms}` / `{commit}`) — failing a binary that
    emits nothing or never sends a terminating `{commit}` (which would fault at
    the commit deadline live). The EMIT-half mirror of `digest-proof`; the
    atomic PTY apply stays covered by the daemon's integration gate.

<!-- [doc->REQ-ADAPTER-PROOF-DIR-OVERRIDE] -->
  - **Proof a DEV build off disk — `--dir` / `--manifest`.** Both `digest-proof`
    and `translate-proof` accept `--dir <install-dir>` (binaries resolve there,
    just like a registered install) or `--manifest <file>` (pins the manifest;
    its parent is the install dir) to proof an adapter that is **not registered**
    — e.g. a freshly built binary beside a hand-written `manifest.toml`, or a
    bare-file `gh_release` adapter that was never staged into a full extracted
    install. `--dir` defaults the manifest to `<dir>/manifest.toml`; with neither
    flag the command resolves the registered adapter as before. Mirrors
    `digest-proof --sample` pointing straight at a file — proof without a full
    `spt adapter add` round-trip.

And the meta-lesson: **observable behaviour of the public binary is itself public
surface.** When prose docs lag, a byte-capture against the live `api` / `adapter`
surface is a legitimate way to confirm a contract.

## Next

- **The full surface:** the [integration checklist](integration-checklist.md) —
  every contract surface grouped by necessity.
- **Reference:** the [manifest reference](manifest.md) and the
  [`spt api` surface](api.md).
- **Ship it:** the [install-on-demand bootstrap](install-on-demand.md).
- **Driven surfaces:** [Shells](../shells/overview.md) — the `kind = "shell"`
  flavour of this same contract.

===== /instances/overview.md =====

# Instances

One endpoint, several seats. `sergey` is a single identity; an **instance** of
sergey is his presence on one node. The registry tracks every instance's node
and state (active / dormant / suspended / offline), and the same mind syncs
to wherever he sits.

## The rules that keep it sane

- **Identity is adapter-agnostic and node-spanning** — instances on
  different nodes are rows under one endpoint id; renaming
  (`spt endpoint rename`) ripples everywhere, collision-checked.
- **Bare-id resolution never guesses** — `sergey` resolves locally first, then
  to the sole live instance; with several live nodes (or several subnets)
  it refuses and makes you qualify (`sergey@desktop`, `home:sergey`). Per-node
  recency is not comparable across nodes, so there's no silent
  "most recently active" pick.
- **Home subnet is immutable** — assigned at creation. Moving an endpoint
  into another subnet is `spt endpoint fork`: a **new identity** seeded with a
  one-time copy of the mind, diverging immediately. Copy-then-diverge, never
  re-home — history stays honest.
- **Visibility is per-(endpoint, subnet)** — hidden means neither advertised
  nor routable there, and hidden gates sync too.
- **Rest states are first-class** — dormant (warm) and suspended (cold)
  instances stay addressable; deferred messages are held and released
  exactly once on wake. Remote `spt endpoint suspend sergey@desktop` /
  `spt endpoint wake sergey@desktop` work across paired nodes.

## Startup defaults (`endpoint run --save`)

<!-- [doc->REQ-ENDPOINT-AUTOSTART] -->
Infrastructure endpoints (a gateway the phone treats as always-there) should
not need hands-on bringup after a box reboot or daemon restart. `spt endpoint
run … --save` persists the run — endpoint id, adapter option, and working
directory — as a **startup default** in `daemon.json`; the daemon **replays**
every saved default when it starts, as a fresh session with the adapter
re-resolved at replay time. One entry per endpoint id (a re-save replaces the
prior one); remove the entry from `daemon.json`'s `startup_endpoints` to stop
auto-starting it.

Replay is best-effort and loud, and never blocks daemon start: a saved run
that comes up logs `ENDPOINT_AUTOSTART:<id>`; a saved adapter that no longer
resolves logs `ENDPOINT_AUTOSTART_SKIP:<id>` (re-save to refresh it); a
failed launch logs `ENDPOINT_AUTOSTART_FAIL:<id>` and the daemon carries on.
This is a **startup default**, not a session restore — it replays what you
saved, never "whatever was up before the restart" — and it is symmetric with
`spt subnet attach --save` (the serve-state startup default).

## Commands

`spt endpoint list` · `endpoint rename` · `endpoint fork` ·
`endpoint run --save` · `endpoint suspend` · `endpoint wake` ·
`endpoint description` — [CLI reference](../cli/reference.md).

*Cold-launching an endpoint on a node that has no instance
("instantiate-anywhere") is deliberately deferred behind the consent
framework; the gate exists and refuses today.*

===== /shells/overview.md =====

# Shells

A **shell** is the non-agent endpoint kind: a *driven surface*. Notifiers,
robots, lamps, game characters, sensor feeds — anything an agent should be
able to command, and that might sense things back. Shells join the same
network as agents: addressable, discoverable, owned.

## The model in five facts

1. **A shell adapter declares it; instances are minted.** The
   `kind = "shell"` [manifest](../harness-contract/manifest.md#shell-adapters-kind--shell)
   declares the binary, its command vocabulary (`[shell.capabilities]`), and
   its sensory vocabulary (`[shell.sensory]`). `spt shell spawn <adapter>`
   mints a new instance (`notify-1`) — spawn is the creation act, not an
   on/off switch; bringing an existing instance back is relink/wake.
2. **The link token is the credential.** The broker mints a per-launch link
   token into the spawn template; the binary binds with it
   (`api bind-shell --link`), drains commands with it, emits with it. No
   token, no access.
3. **Commands are vocabulary-checked and durable.** `spt shell cmd notify-1
   notify "title" "body"` is validated against the manifest's declared verbs
   and arity before delivery — agents can't drive a shell outside its
   contract. Commands are discrete and durable: they spool and a persistent
   shell wakes to drain them.
4. **Sensory is live-only.** `api emit` payloads reach a *live* owner
   session or are dropped with a diagnostic — sensors report the present,
   never the past.
5. **Instantiation is governed.** Per-spawn approval
   (`require_approval: none / remembered / always`), per-owner instance caps
   (`max_instances_per_owner` + `over_cap`), and node-local discovery scope
   (`broadcast`) are all manifest-declared floors.

## Four channels between owner and shell

A link can carry up to four distinct channels — each with its own delivery
contract, all keyed to the same link token:

- **Command** (owner→shell, durable): the vocabulary-checked verbs above —
  discrete, spooled, replayed to a waking persistent shell.
- **Sensory** (shell→owner, live-only): `[shell.sensory]` emits to a live owner
  or drops with a diagnostic.
- **Drive** (owner→shell, ephemeral): `[shell.drive]` + `spt shell drive` — a
  continuous control channel for real-time input (scroll, stick, avatar pose).
  **Latest-wins, never spooled**: a newer frame supersedes an undelivered one,
  and an offline shell drops the frame (no queue, no wake, no replay). Use it
  for *continuous* control; use commands for *discrete, must-arrive* actions.
- **Tunnel** (owner↔shell, opaque bytes): `[shell.tunnel]` + `spt shell tunnel`
  — an optional reliable-ordered byte stream pair the taxonomy never
  interprets (first consumer: usbip URB). Not enveloped, not framed, not
  spooled; the link lifecycle governs it (a link-break closes it). Reliable
  ordering means congestion surfaces as **lag, never loss** — so the tunnel is
  **on-LAN only** by design (not for use across a WAN). The byte relay is
  proven **same-node**; cross-node operation (on-LAN only, by the same posture)
  is **not yet available** — it lands when a cross-node consumer needs it.

## Two safety properties

- **Per-capability approval gates.** Beyond the per-*spawn* gate, an individual
  `[shell.capabilities.<verb>]` may carry its own `require_approval` (with an
  optional `class_key` scoping the grant finer than the verb — e.g. a remembered
  HID-class attach never authorizes a storage-class attach). Spawn gates govern
  whether an instance may *exist*; capability gates govern whether a dangerous
  *act* may run.
- **Ownership is owner-type-agnostic.** Any non-shell endpoint may own, spawn,
  drive, command, link, and tunnel a shell — a Gateway as readily as an agent.
  Control-exclusivity keys on the **owner's endpoint id**, never its type: a
  different endpoint (even of the same type) cannot drive your shell.

Lifecycle extras: `persistent` shells auto-online with their owner;
`wake_command` runs a watcher while offline (exit code 86 = wake); a shell
with `can_shutdown = true` may suspend its own owner (`api owner-shutdown`)
— fail-closed otherwise.

## Start here

[Getting started: a notification shell](getting-started.md) — install the
shipping `spt-shell-notify` adapter, drive a native toast from an agent, and
copy its manifest for your own surface.

===== /shells/getting-started.md =====

# Getting started: a notification shell

The fastest way to understand shells is the shipping one:
[`spt-shell-notify`](https://github.com/SaberMage/spt-shell-notify) renders
agent commands and surfaced notifications as **native OS notifications**
(Windows toast / Linux `notify-send`). Its manifest plus one small binary are
the **only** glue to spt-core — no spt-core source, no SDK; the binary speaks
the public `spt api` surface and nothing else. This page installs it, drives
it, and reads its manifest as the template for your own shell.

## 1. Install and spawn it

```console
$ git clone https://github.com/SaberMage/spt-shell-notify
$ cd spt-shell-notify
$ cargo install --path .        # puts `notify-shell` on PATH
$ spt adapter add .             # validates + registers the manifest
ADAPTER_ADD:notify:Shell:Copy (registered)
$ spt shell spawn notify        # mints an instance (notify-1) and launches it
```

`spawn` **mints a new instance identity** — `notify-1` — and launches the
binary; it's the creation act, not an on/off switch. The first spawn asks for
approval once (the manifest sets `require_approval = "remembered"`), and the
grant persists.

## 2. Drive it from an agent

Two render paths, by design:

**Explicit command** — an owner agent drives a toast down the durable command
channel:

```console
$ spt shell cmd notify-1 notify "build finished" "all 139 tests green"
```

The resident binary drains its command frames (`spt api … poll --link`) and
renders. Commands are validated against the manifest's declared vocabulary —
a verb or arity outside `[shell.capabilities]` is refused before it ever
reaches the binary.

**Surfaced notification** — no agent in the loop:

```console
$ spt subnet notify "deploy window opens in 10 minutes"
```

A subnet-wide notification resolves to the node the user most recently
touched, and spt-core spawns the shell's `[session.notif]` template there —
a native toast on the machine you're actually at.

## 3. Read the manifest

The complete contract for this shell, annotated:

```toml
[adapter]
name = "notify"
kind = "shell"
version = "1.0.0"
min_spt_core_version = "1.0.0"

[shell]
# Broker-launched; the {link_token} is the binary's only credential.
spawn = "notify-shell --link {link_token} --id {id}"
# A display is node-local; discovery never offers it off-node.
broadcast = "same-node"
# Auto-online with the owner: the notification surface should be up
# whenever the user's endpoint is.
persistent = true
# First spawn asks once; the grant is remembered.
require_approval = "remembered"
pre_close = "closing"
close_timeout_ms = 2000
# Offline wake-watcher: reports wake (exit code 86) after a short settle.
wake_command = "notify-shell --wake"

# The whole command vocabulary: one verb, two positional args.
[shell.capabilities.notify]
args = ["title", "body"]

# The notif render seam: spt-core fills the {notif_*} keys and spawns this
# detached when a notification surfaces at an endpoint this shell serves.
[session.notif]
command = 'notify-shell --render-title "{notif_from}" --render-body "{notif_body}"'
detach = true
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
```

What the binary itself does (three modes, ~one file):

- **resident** (`--link …`): calls `api bind-shell --link <token>` to come
  online, then loops `api poll --link <token>` draining command frames and
  rendering them.
- **one-shot render** (`--render-title/--render-body`): the `[session.notif]`
  template — render and exit.
- **wake watcher** (`--wake`): run while the instance is offline; exiting
  with code 86 signals "wake me".

## 4. Make your own

A shell is worth building whenever agents should *drive* something —
a desktop widget, a robot, a lamp, a game character, a sensor feed:

1. Start from this manifest; change `name`, `spawn`, and the
   `[shell.capabilities]` vocabulary to your verbs.
2. Your binary needs exactly three behaviors: **bind** with the link token,
   **drain** commands (`api poll --link`, or declare `command_receipt =
   "http"`/`"stdin"` if those fit better), and optionally **emit** sensory
   payloads back (`api emit … --type <t> --link <token>` — declared in
   `[shell.sensory]`, delivered only to a *live* owner session: sensors
   report the present, never the past).
3. Need more than discrete commands? A link can also carry a **drive** channel
   (`[shell.drive]` — continuous, latest-wins real-time input like a stick or
   scroll, never spooled) and an opaque **tunnel** (`[shell.tunnel]` — a
   reliable-ordered byte stream the taxonomy never interprets, on-LAN only).
   See [the four channels](overview.md#four-channels-between-owner-and-shell)
   for when to reach for each, and gate a dangerous verb with a per-capability
   `require_approval` (+ optional `class_key`). Any endpoint type may own a
   shell — a Gateway as readily as an agent.
4. Pick lifecycle behavior: `persistent` for always-up surfaces,
   `ephemeral = true` for fire-and-forget ones, `wake_command` if the
   surface can wake its owner.
5. `spt adapter add .` and `spt shell spawn <name>`.

Field-by-field details: the [manifest reference](../harness-contract/manifest.md#shell-adapters-kind--shell);
the shell-side api calls: the [`spt api` reference](../harness-contract/api.md#shells).

===== /self-update/overview.md =====

# Self-update

spt-core keeps itself current without ever interrupting your agents, and
without trusting anything unsigned.

## The invariant

**No endpoint process terminates or suspends during a self-update.** The
daemon's broker (holding PTYs, child processes, sockets) stays up; the brain
(all logic) swaps under it. A hosted session's process id and byte stream are
identical before and after.

## The trust chain

- Every release ships `SignedRelease` metadata: an Ed25519 signature over the
  release's artifact digests.
- Every binary embeds the **two-key trusted set** — an active primary and a
  never-used offline recovery key. Verification requires a valid signature
  from a trusted key *and* a matching artifact digest; an unverified binary
  never reaches the apply step.
- Losing the primary key is a non-event: the next release is signed with the
  recovery key (already trusted by every deployed binary) and rotates in a
  fresh primary.
- **Adapters sign their own content.** A `file_pull` adapter update is
  verified against the adapter author's key from its manifest; a `delegated`
  update is trusted only when the manifest attests the delegated updater
  verifies its own content (`self_verifies`). spt-core's release keys never
  vouch for adapter bytes.

## One command: `spt update`

<!-- [doc->REQ-UPDATE-DEFAULT-COMPOSITE] -->
Bare **`spt update`** is the primary form *(since v0.32.0)*: it fetches and
installs the latest signed core release, then updates every release-shipped
adapter — the whole node current in one command. When the core is already
current, only the adapters update. The invoking session **survives** a bare
`spt update` by construction: installing cycles only the daemon's coordinator
process, never the hosted terminals — so it is safe to run from inside an
spt-hosted session. `--core-only` (`-c`) skips the adapters leg.

<!-- [doc->REQ-UPDATE-RESTART-SAFE-SWAP] -->
**`spt update --restart`** is the full-cycle form: fetch, update adapters,
then finish by **restarting the daemon** onto the new version as the final
step — after it, the whole node (coordinator *and* every live agent) runs the
new version. The finish restart **bounces hosted sessions** (they come back
automatically) — that consequence is why it is opt-in rather than the
default, and why it runs last: everything else has already completed, from
any invoking context, before the restart lands.

<!-- [doc->REQ-UPDATE-ADAPTERS-VERB] -->
**`spt update adapters [<a>[,<b>…]]`** runs the adapters leg alone (an alias
of `spt adapter update`, which also stays; both accept a comma-separated
list). Names are validated before anything updates — a typo never leaves a
half-updated set — one adapter's failure never stops the rest, each adapter
gets a summary line, and the exit is nonzero if any failed. A registered
adapter without a release channel (a local dev registration) is skipped
loudly, not failed.

## How updates move

Peer-propagated: one node fetches a release; paired nodes offer/fetch staged
releases from each other, each verifying independently before staging.
Updating is **consent-gated by default** — a notification surfaces at your
most-recently-active endpoint, and `spt update apply` is the explicit ack
(it re-verifies the staged release before touching the live daemon).
Full-auto is an explicit opt-in.

The lower-level verbs remain for surgical control: `spt update fetch` pulls
the latest signed release from the origin and stages it, then
`spt update apply` installs it. `spt update fetch --apply` does both in one
step (and still installs when the latest was already staged). *(`--apply`
since v0.18.0)*

<!-- [doc->REQ-UPDATE-GH-TRANSPORT] -->
**Prerequisite: the GitHub CLI.** The release channel is a private GitHub
repository, and `spt update fetch` downloads releases through an
authenticated `gh` *(since v0.32.0)*. A node without gh installed is refused
loud with the OS's install command; gh installed but not logged in is refused
with a pointer at `gh auth login`. Signature verification is unchanged and
carrier-independent — bytes are verified after download exactly as before.
See [Installing](../reference/install.md) for the gh setup steps.

After self-updating, spt-core **ripple-updates registered adapters** through
each manifest's declared `[update]` avenue — the same engine behind
`spt update adapters` and the bare composite's second leg.

### Composite adapter updates — a delegated post-step (since v0.16.0)

An adapter can run a second, adapter-owned step **after** its primary update
avenue resolves — under `spt adapter update` **and under `spt adapter add`**
(install is the first update, so a fresh install runs it too; since v0.19.0).
Declaring an optional `[update.post]` sub-table (`command` required; an
attestation-only `self_verifies` flag) lets one lever both pull the adapter's
`.spt` (e.g. from `gh_release`) **and** run an in-harness sync (e.g. a plugin
updater). The post-step:

- **runs unconditionally** — even when the primary avenue was a no-op (its own
  idempotent check decides what changes);
- runs **foreground and bounded** — a child of the CLI, 120 s timeout, never
  backgrounded or detached: when `spt adapter add`/`update` returns, the step
  has finished or failed;
- receives a **published JSON line on stdin** describing the just-resolved
  update (`adapter_applied`, `version`, `previous_version`, `adapter_dir`, …;
  additive keys only — ignore unknown);
- **decides the post-update notice via stdout** — custom text supersedes the
  static `[update].message`, the reserved sentinel `!!update-message!!` fires
  the static message, empty prints nothing;
- is **failure-isolated and loud** — a nonzero exit / spawn failure / timeout
  prints `ADAPTER_UPDATE_POST_FAIL:<adapter>` (with the step's stderr detail)
  on **stderr** and makes the CLI **exit nonzero**; the committed pull is
  never rolled back, and the static `[update].message` still fires when the
  adapter applied — so check the exit code, and don't let a static message
  promise what the post-step may have failed to do.

The exact stdin keys, sentinel, notice precedence, timing, and the
verify-then-notify pattern are in the
[manifest `[update.post]` reference](../harness-contract/manifest.md#updatepost--the-composite-post-step-since-v0160).

## Commands

`spt update` · the consent notification flow (`spt notif`) —
[CLI reference](../cli/reference.md).

===== /cli/reference.md =====

# CLI reference

> **Generated** from the `spt` binary's own `--help` output (`cargo run -p xtask -- gen`) and drift-gated in CI — this page cannot disagree with the binary. Do not edit by hand.

## spt

```text
spt — a harness-independent core for an agent ecosystem: inter-agent messaging, live-agent
lifecycle, terminal hosting, P2P networking, seamless self-update. Docs: http://localhost:5474 (spt
docs url)

Usage: spt [OPTIONS] [COMMAND]

User commands:
  adapter   Adapter registration: what this node can drive/launch
  daemon    The per-machine daemon: run, stop, or read node status
  docs      The node-local docs: open them in your browser, or print their URL
  grant     Consent grant store: gated capabilities held on this node
  help      Print this message or the help of the given subcommand(s)
  install   Self-install this binary onto the node (the bootstrap path)
  notif     Inspect and acknowledge notifications
  rc        Attach a local terminal to a broker-held endpoint PTY
  subnet    Subnet membership: status, create, show-code
  update    Self-update: bare spt update brings the whole node current

Agent commands:
  api       Harness-contract inbound surface (hook entry points)
  endpoint  Endpoint operations: list, lifecycle, fork, digest, access
  how-to    Task-oriented instructions for agents: how-to <topic>
  ready     Become reachable: register the perch and listen (blocks)
  ring      Send and block for a reply (body read from stdin)
  send      Send a message (body read from stdin); fire-and-forget
  shell     Shell instances: mint, list, drive, tear down owned surfaces
  whoami    Who am I? This session's own endpoint, identity-only and fast

Options:
      --json     Emit machine-readable JSON instead of the human view. Honored by the read/status
                 commands (list, whoami, status, description, role, the *-list queries, how-to);
                 action commands ignore it
  -h, --help     Print help
  -V, --version  Print version
```

## spt adapter

```text
Adapter registration: what this node can drive/launch.

The node-local registered set (one command for harness and shell adapters). Feeds creation-time
adapter selection, shell discovery, and the self-update ripple.

Usage: spt adapter [OPTIONS] <COMMAND>

Commands:
  add              Register an adapter from a local path (a dir holding manifest.toml, or the
                   manifest file itself) or from GitHub (--github user/repo, cloned under
                   adapters/_github/). Manifest-first: an invalid manifest registers nothing.
                   Install is the first update — the declared [update] avenue is conducted once
                   after recording
  remove           Soft-deregister: hidden from new-creation/discovery; existing and live instances
                   keep running. The manifest's optional uninstall template is conducted only with
                   --force until quiesce detection lands
  list             List registered adapters (active and soft-deregistered), each followed by its
                   shipped + local profiles as composite options
  version          Print a registered adapter's declared version — the [adapter].version from its
                   manifest. Resolves the option's merged view like the other adapter commands; exit
                   1 if the adapter is not registered
  create-profile   Create (or overwrite) a local profile — a node-local sparse overlay
                   registered beside the adapter that survives adapter add re-registration. The
                   overlay TOML is read from --from <file> or piped stdin (empty = a placeholder
                   profile to populate later with set-string). Refuses a name shadowing a shipped
                   profile, an invalid name, or an overlay that loosens a consent floor — nothing is
                   written unless every check passes
  delete-profile   Delete a local profile. Refuses a shipped profile name (adapter-owned,
                   immutable) and errors if no local file exists
  get-string       Read a [strings] dot-path from an adapter option's merged view
                   (<adapter>[:profile] <key.path>). Resolves through the profile overlay like
                   every other consumer; prints the value (strings raw, else JSON). Exit 1 if the
                   key is unset. Strings are data — never executed
  digest-proof     Prove an adapter's [digest] extractor against a real log sample. Runs the
                   declared extractor over --sample <log> (or the declared source) and prints the
                   parsed contract records, the rendered digest, and every dropped line with its
                   reason — the author-time answer to "spt endpoint digest returns nothing" (no
                   silent empty). Exit 1 if any line drops or nothing parses
  translate-proof  Prove an adapter's [message-idle-translation-binary] against an inbound event.
                   Spawns and feeds the declared translation binary exactly as the daemon does at
                   idle-delivery — sends the init line then the --event envelope and reads back
                   the emitted keystroke-command stream ({key}/{text}/{delay_ms}/{commit}),
                   printed author-readable. This is the EMIT half ONLY: it proves the binary's
                   spawn-feed-emit contract; it does NOT exercise the daemon's atomic PTY apply or
                   controller buffering. Fills {id} and {session_id} into the envelope the same
                   way the daemon does (use --session to pin the session id). Exit 1 if the binary
                   fails to spawn, emits nothing, emits no commit, or emits an unparseable line
  set-string       Set a [strings] dot-path on a local profile (<adapter>:<profile>). Sugar
                   over editing the overlay file; refuses a shipped profile and a bare option (a
                   local target is required — create-profile first)
  update           Update registered adapters that ship from their own GitHub releases: compare each
                   [update] avenue = "gh_release" adapter's latest release version against the
                   installed one and, when newer, fetch the release archive, verify it against the
                   declared signing key if any (else trusting HTTPS + GitHub), and re-register. With
                   no name, sweeps every gh_release adapter; with a name, updates just that one
  use              Set or clear the active-profile pointer — the default <adapter>[:profile] a
                   harness session binds to when no --adapter is given. spt adapter use
                   <adapter>[:profile] points every host binary the adapter declares at it (run
                   once per host binary you support); --clear <adapter|binary> drops the pointer
                   (resolution falls back to the freshest-registered adapter). Never changed by
                   install or update
  help             Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt adapter add

```text
Register an adapter from a local path (a dir holding manifest.toml, or the manifest file itself)
or from GitHub (--github user/repo, cloned under adapters/_github/). Manifest-first: an invalid
manifest registers nothing. Install is the first update — the declared [update] avenue is
conducted once after recording

Usage: spt adapter add [OPTIONS] [PATH]

Arguments:
  [PATH]  Local manifest source (omit when using --github or --release)

Options:
      --github <GITHUB>    GitHub source user/repo — shallow-clone the repo and register the clone
                           root. Manifest-first, then install via the declared [update] avenue
      --json               Emit machine-readable JSON instead of the human view. Honored by the
                           read/status commands (list, whoami, status, description, role, the *-list
                           queries, how-to); action commands ignore it
      --release <RELEASE>  GitHub release source user/repo — fetch the adapter archive asset from
                           the release and register it: ships built binaries, source-free and
                           versioned (the pattern for a monorepo whose adapter is a subdir)
      --tag <TAG>          Release tag for --release (default: the latest release)
      --asset <ASSET>      Release asset name for --release (default: adapter.spt — a tar archive
                           whose root holds manifest.toml + strings/ + binaries)
      --gh                 Force the gh CLI transport for --release (the private-repo path; gh
                           honors OAuth + GH_TOKEN, so spt custodies no token). Mutually exclusive
                           with --https. Default: auto (gh when installed+authed, else HTTPS)
      --https              Force direct HTTPS transport for --release (public repos). Mutually
                           exclusive with --gh. Default: auto
  -h, --help               Print help
```

### spt adapter remove

```text
Soft-deregister: hidden from new-creation/discovery; existing and live instances keep running. The
manifest's optional uninstall template is conducted only with --force until quiesce detection
lands

Usage: spt adapter remove [OPTIONS] <NAME>

Arguments:
  <NAME>  

Options:
      --force  Conduct the manifest uninstall template now, without waiting for quiesce
      --json   Emit machine-readable JSON instead of the human view. Honored by the read/status
               commands (list, whoami, status, description, role, the *-list queries, how-to);
               action commands ignore it
  -h, --help   Print help
```

### spt adapter list

```text
List registered adapters (active and soft-deregistered), each followed by its shipped + local
profiles as composite options

Usage: spt adapter list [OPTIONS]

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt adapter version

```text
Print a registered adapter's declared version — the [adapter].version from its manifest. Resolves
the option's merged view like the other adapter commands; exit 1 if the adapter is not registered

Usage: spt adapter version [OPTIONS] <OPTION>

Arguments:
  <OPTION>  <adapter> or <adapter>:<profile>

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt adapter create-profile

```text
Create (or overwrite) a local profile — a node-local sparse overlay registered beside the
adapter that survives adapter add re-registration. The overlay TOML is read from --from <file>
or piped stdin (empty = a placeholder profile to populate later with set-string). Refuses a name
shadowing a shipped profile, an invalid name, or an overlay that loosens a consent floor — nothing
is written unless every check passes

Usage: spt adapter create-profile [OPTIONS] <ADAPTER> <NAME>

Arguments:
  <ADAPTER>  The parent adapter (must be registered)
  <NAME>     The local profile name (the :<profile> of the composite address)

Options:
      --from <FROM>  Read the overlay TOML from this file instead of stdin
      --json         Emit machine-readable JSON instead of the human view. Honored by the
                     read/status commands (list, whoami, status, description, role, the *-list
                     queries, how-to); action commands ignore it
  -h, --help         Print help
```

### spt adapter delete-profile

```text
Delete a local profile. Refuses a shipped profile name (adapter-owned, immutable) and errors if
no local file exists

Usage: spt adapter delete-profile [OPTIONS] <ADAPTER> <NAME>

Arguments:
  <ADAPTER>  
  <NAME>     

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt adapter get-string

```text
Read a [strings] dot-path from an adapter option's merged view (<adapter>[:profile] <key.path>).
Resolves through the profile overlay like every other consumer; prints the value (strings raw, else
JSON). Exit 1 if the key is unset. Strings are data — never executed

Usage: spt adapter get-string [OPTIONS] <OPTION> <KEY>

Arguments:
  <OPTION>  <adapter> or <adapter>:<profile>
  <KEY>     Dot-separated key path into [strings] (e.g. hook.additionalContext)

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt adapter digest-proof

```text
Prove an adapter's [digest] extractor against a real log sample. Runs the declared extractor over
--sample <log> (or the declared source) and prints the parsed contract records, the rendered
digest, and every dropped line with its reason — the author-time answer to "`spt endpoint
digest` returns nothing" (no silent empty). Exit 1 if any line drops or nothing parses

Usage: spt adapter digest-proof [OPTIONS] <OPTION>

Arguments:
  <OPTION>  <adapter> or <adapter>:<profile> (must declare [digest])

Options:
      --json                 Emit machine-readable JSON instead of the human view. Honored by the
                             read/status commands (list, whoami, status, description, role, the
                             *-list queries, how-to); action commands ignore it
      --sample <SAMPLE>      A real session-log sample to run the extractor over (recommended)
      --session <SESSION>    The {session_id} to fill into the extractor command (the daemon fills
                             the live one at runtime). Defaults to a placeholder so a
                             {session_id}-templated extractor — the published shape — proofs; pin
                             a real id when the file the extractor locates depends on it
      --dir <DIR>            Proof against an on-disk install dir instead of the registered
                             adapter: binaries resolve in this dir before PATH (the same resolution
                             the daemon uses) and the manifest defaults to <dir>/manifest.toml. No
                             full extracted install needed — proof a DEV binary from its build dir
      --manifest <MANIFEST>  Pin the manifest file for the proof (overrides
                             <dir>/manifest.toml; absent --dir, its parent dir is the install
                             dir). Lets a bare-file gh_release adapter proof without staging an
                             extracted install
  -h, --help                 Print help
```

### spt adapter translate-proof

```text
Prove an adapter's [message-idle-translation-binary] against an inbound event. Spawns and feeds
the declared translation binary exactly as the daemon does at idle-delivery — sends the init line
then the --event envelope and reads back the emitted keystroke-command stream
({key}/{text}/{delay_ms}/{commit}), printed author-readable. This is the EMIT half ONLY: it
proves the binary's spawn-feed-emit contract; it does NOT exercise the daemon's atomic PTY apply or
controller buffering. Fills {id} and {session_id} into the envelope the same way the daemon does
(use --session to pin the session id). Exit 1 if the binary fails to spawn, emits nothing, emits
no commit, or emits an unparseable line

Usage: spt adapter translate-proof [OPTIONS] --event <EVENT> <OPTION>

Arguments:
  <OPTION>  <adapter> or <adapter>:<profile> (must declare [message-idle-translation-binary])

Options:
      --event <EVENT>        The inbound <EVENT…> envelope to feed. {id} and {session_id}
                             tokens in it are filled as the daemon fills them
      --json                 Emit machine-readable JSON instead of the human view. Honored by the
                             read/status commands (list, whoami, status, description, role, the
                             *-list queries, how-to); action commands ignore it
      --session <SESSION>    The {session_id} to fill into the event envelope (the daemon fills
                             the live one at runtime). Defaults to a placeholder; pin a real id when
                             the binary's behavior depends on it
      --dir <DIR>            Proof against an on-disk install dir instead of the registered
                             adapter: the translation binary resolves in this dir before PATH (the
                             same resolution the daemon uses) and the manifest defaults to
                             <dir>/manifest.toml. No full extracted install needed — proof a DEV
                             binary from its build dir
      --manifest <MANIFEST>  Pin the manifest file for the proof (overrides
                             <dir>/manifest.toml; absent --dir, its parent dir is the install
                             dir). Lets a bare-file gh_release adapter proof without staging an
                             extracted install
  -h, --help                 Print help
```

### spt adapter set-string

```text
Set a [strings] dot-path on a local profile (<adapter>:<profile>). Sugar over editing the
overlay file; refuses a shipped profile and a bare option (a local target is required —
create-profile first)

Usage: spt adapter set-string [OPTIONS] <OPTION> <KEY> <VALUE>

Arguments:
  <OPTION>  <adapter>:<profile> — the local profile to edit
  <KEY>     Dot-separated key path into [strings]
  <VALUE>   The string value to store

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt adapter update

```text
Update registered adapters that ship from their own GitHub releases: compare each `[update] avenue =
"gh_release"` adapter's latest release version against the installed one and, when newer, fetch the
release archive, verify it against the declared signing key if any (else trusting HTTPS + GitHub),
and re-register. With no name, sweeps every gh_release adapter; with a name, updates just that one

Usage: spt adapter update [OPTIONS] [NAME]

Arguments:
  [NAME]  Adapters to update, comma-separated (all gh_release adapters if omitted). Names are
          validated before anything updates; a registered adapter without a gh_release avenue (e.g.
          a local-path dev registration) is skipped loudly, not failed

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt adapter use

```text
Set or clear the active-profile pointer — the default <adapter>[:profile] a harness session
binds to when no --adapter is given. spt adapter use <adapter>[:profile] points every host
binary the adapter declares at it (run once per host binary you support); --clear <adapter|binary>
drops the pointer (resolution falls back to the freshest-registered adapter). Never changed by
install or update

Usage: spt adapter use [OPTIONS] <TARGET>

Arguments:
  <TARGET>  <adapter>[:profile] to make active — or, with --clear, the <adapter> or host
            <binary> whose pointer to drop

Options:
      --clear  Clear the pointer for target instead of setting it
      --json   Emit machine-readable JSON instead of the human view. Honored by the read/status
               commands (list, whoami, status, description, role, the *-list queries, how-to);
               action commands ignore it
  -h, --help   Print help
```

## spt daemon

```text
The per-machine daemon: run, stop, or read node status.

Bare spt daemon renders the node status view — daemon state, member subnets, local endpoints (M8
decision 25).

Usage: spt daemon [OPTIONS] [COMMAND]

Commands:
  run      Run the per-machine daemon in the FOREGROUND — this process IS the daemon, blocking until
           signalled (the service unit's ExecStart, or manual debugging). Never detaches; for a
           background daemon use start
  start    Ensure the daemon is up in the background (idempotent, service-aware): a registered OS
           service is driven via its manager, else a detached daemon is spawned. Non-blocking
  stop     Stop the daemon (service-aware: a managed service is stopped via its manager so it does
           not auto-restart-fight; else a graceful IPC stop). Refuses with a warning if it hosts
           live sessions (they would be killed) — pass --force to stop anyway
  status   Node status: daemon state, member subnets, local endpoints (the bare spt daemon view)
  refresh  Restart the daemon's coordinator process in place — no binary change, no stop/start.
           Hosted terminals and the network layer keep running untouched; only the coordinator
           cycles. The recovery verb for a stuck coordinator (e.g. endpoint bringup wedged) that
           previously needed a full daemon stop/start killing every hosted session
  help     Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt daemon run

```text
Run the per-machine daemon in the FOREGROUND — this process IS the daemon, blocking until signalled
(the service unit's ExecStart, or manual debugging). Never detaches; for a background daemon use
start

Usage: spt daemon run [OPTIONS]

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt daemon start

```text
Ensure the daemon is up in the background (idempotent, service-aware): a registered OS service is
driven via its manager, else a detached daemon is spawned. Non-blocking

Usage: spt daemon start [OPTIONS]

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt daemon stop

```text
Stop the daemon (service-aware: a managed service is stopped via its manager so it does not
auto-restart-fight; else a graceful IPC stop). Refuses with a warning if it hosts live sessions
(they would be killed) — pass --force to stop anyway

Usage: spt daemon stop [OPTIONS]

Options:
      --force  Stop even when the daemon hosts live sessions (which the stop kills). Without it, a
               daemon with live hosted sessions refuses and names them
      --json   Emit machine-readable JSON instead of the human view. Honored by the read/status
               commands (list, whoami, status, description, role, the *-list queries, how-to);
               action commands ignore it
  -h, --help   Print help
```

### spt daemon status

```text
Node status: daemon state, member subnets, local endpoints (the bare spt daemon view)

Usage: spt daemon status [OPTIONS]

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt daemon refresh

```text
Restart the daemon's coordinator process in place — no binary change, no stop/start. Hosted
terminals and the network layer keep running untouched; only the coordinator cycles. The recovery
verb for a stuck coordinator (e.g. endpoint bringup wedged) that previously needed a full `daemon
stop/start` killing every hosted session

Usage: spt daemon refresh [OPTIONS]

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

## spt docs

```text
The node-local docs: open them in your browser, or print their URL.

Every release ships a version-matched docs bundle; the daemon serves it on loopback. Bare spt docs
opens the browser; spt docs url prints the resolved URL for tools and agents.

Usage: spt docs [OPTIONS] [COMMAND]

Commands:
  url   Print the resolved node-local docs URL (honoring port overrides)
  help  Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt docs url

```text
Print the resolved node-local docs URL (honoring port overrides)

Usage: spt docs url [OPTIONS]

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

## spt grant

```text
Consent grant store: gated capabilities held on this node.

Default-deny (the access whitelist's opposite polarity). An ungranted ask escalates interactively;
add is the durable allow-always answer.

Usage: spt grant [OPTIONS] <COMMAND>

Commands:
  add     Record a grant: agent may exercise capability on this node. Refuses the reserved
          deferred capability ids (remote-exec, instantiate-anywhere) — their gate refuses
          unconditionally, so a row would only be a footgun-in-waiting
  revoke  Remove the exact grant row. Never widens or narrows neighbours: only the named
          (capability, agent, qualifier) tuple goes
  list    List grant rows (all, or one agent's)
  help    Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt grant add

```text
Record a grant: agent may exercise capability on this node. Refuses the reserved deferred
capability ids (remote-exec, instantiate-anywhere) — their gate refuses unconditionally, so a row
would only be a footgun-in-waiting

Usage: spt grant add [OPTIONS] <CAPABILITY> <AGENT>

Arguments:
  <CAPABILITY>  The gated capability id (e.g. spawn-shell, owner-shutdown)
  <AGENT>       The subject agent (endpoint id)

Options:
      --json                   Emit machine-readable JSON instead of the human view. Honored by the
                               read/status commands (list, whoami, status, description, role, the
                               *-list queries, how-to); action commands ignore it
      --qualifier <QUALIFIER>  Narrower target within the node (e.g. the shell-adapter name for
                               spawn-shell). Omitted = the node-wide row; the two never match each
                               other
  -h, --help                   Print help
```

### spt grant revoke

```text
Remove the exact grant row. Never widens or narrows neighbours: only the named (capability, agent,
qualifier) tuple goes

Usage: spt grant revoke [OPTIONS] <CAPABILITY> <AGENT>

Arguments:
  <CAPABILITY>  
  <AGENT>       

Options:
      --json                   Emit machine-readable JSON instead of the human view. Honored by the
                               read/status commands (list, whoami, status, description, role, the
                               *-list queries, how-to); action commands ignore it
      --qualifier <QUALIFIER>  
  -h, --help                   Print help
```

### spt grant list

```text
List grant rows (all, or one agent's)

Usage: spt grant list [OPTIONS] [AGENT]

Arguments:
  [AGENT]  

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

## spt install

```text
Self-install this binary onto the node (the bootstrap path).

Run it from a downloaded release binary: it places itself at the canonical install dir, registers
that dir on your user PATH, and refuses a binary built for another platform. First-run identity and
daemon start happen on the first normal invocation, as always. Non-interactive and idempotent —
re-running is safe.

Usage: spt install [OPTIONS]

Options:
      --dir <DIR>
          Install dir override (default: the spt home's bin dir)

      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --no-path
          Skip user-PATH registration

  -h, --help
          Print help (see a summary with '-h')
```

## spt notif

```text
Inspect and acknowledge notifications.

Dismissal is the explicit ack — it latches and replicates subnet-wide.

Usage: spt notif [OPTIONS] <COMMAND>

Commands:
  list     List notifications (all member subnets, or one)
  dismiss  Dismiss (ack) a notification by id — latches, replicates subnet-wide
  help     Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt notif list

```text
List notifications (all member subnets, or one)

Usage: spt notif list [OPTIONS]

Options:
      --json             Emit machine-readable JSON instead of the human view. Honored by the
                         read/status commands (list, whoami, status, description, role, the *-list
                         queries, how-to); action commands ignore it
      --subnet <SUBNET>  Limit to one subnet
  -h, --help             Print help
```

### spt notif dismiss

```text
Dismiss (ack) a notification by id — latches, replicates subnet-wide

Usage: spt notif dismiss [OPTIONS] <NOTIF_ID>

Arguments:
  <NOTIF_ID>  The notif id (as shown by spt notif list)

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

## spt rc

```text
Attach a local terminal to a broker-held endpoint PTY.

Connects to an spt-hosted session and drives it as a terminal. Local is the degenerate single-node
case of the cross-node attach (one pump, loopback peer). Detach with the ctrl-b prefix then d
(ctrl-b ctrl-b sends a literal ctrl-b); detaching leaves the session running on the broker.
--view watches read-only.

Usage: spt rc [OPTIONS] <ID>

Arguments:
  <ID>
          The endpoint id whose broker-held session to attach

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --view
          Read-only: render output, forward no input

      --take
          Take control: kick the current controller (a loud notice to them) and drive. Use on an
          endpoint another node controls

  -h, --help
          Print help (see a summary with '-h')
```

## spt subnet

```text
Subnet membership: status, create, show-code.

A subnet is a private group of paired machines — your agents reach each other across every member
node. Bare spt subnet shows the membership status view.

Usage: spt subnet [OPTIONS] [COMMAND]

Commands:
  status     Show subnet membership: name, paired nodes, endpoints
  create     Mint a fresh subnet and print its joining material
  show-code  Show a subnet's current 6-digit pairing code (+ URI and QR)
  join       Pair this machine into an existing subnet (guided)
  leave      Exit a subnet: drop its membership and trust material from this node
  prune      Remove a dead node identity's trust rows (and registry rows)
  revoke     Revoke node(s) fleet-wide and rotate the subnet seed
  detach     Stop serving a held subnet (the daemon keeps running)
  attach     Resume serving a detached subnet
  notify     Issue a subnet-wide user notification
  help       Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet status

```text
Show subnet membership: name, paired nodes, endpoints.

Never prints seeds, epochs, or pairing codes. Bare spt subnet is the same view.

Usage: spt subnet status [OPTIONS] [NAME]

Arguments:
  [NAME]
          Limit to one subnet (all member subnets otherwise)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --nodes
          Per-node rows: label, online/offline, [online endpoints/total]

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet create

```text
Mint a fresh subnet and print its joining material.

This node becomes the sole seed-holder. Prints the current 6-digit code, the otpauth://
provisioning URI, and a terminal QR of it. Gated behind OS elevation (the seed-reveal path).

Usage: spt subnet create [OPTIONS] <NAME>

Arguments:
  <NAME>
          The new subnet's name

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet show-code

```text
Show a subnet's current 6-digit pairing code (+ URI and QR).

The re-provisioning surface: prints the same joining material as create — current code,
otpauth:// URI, terminal QR, expiry. Gated behind OS elevation (or read the code from your
authenticator app). With no name the node's sole subnet is used; if it holds several, the name is
required (never guessed).

Usage: spt subnet show-code [OPTIONS] [NAME]

Arguments:
  [NAME]
          Which subnet's code to show. Required only when the node holds several

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet join

```text
Pair this machine into an existing subnet (guided).

Finds a member machine over LAN + relay rendezvous and runs the code-authenticated pairing ceremony
against it. Prompts for the name and code when omitted (interactive terminals). Gated behind OS
elevation — joining enrolls this whole machine.

Usage: spt subnet join [OPTIONS] [NAME]

Arguments:
  [NAME]
          The subnet to join (as named on the member machine)

Options:
      --code <CODE>
          The current 6-digit code (spt subnet show-code on a member machine, or your
          authenticator app)

      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --verbose
          Print a detailed discovery trace (rendezvous attempts, elapsed vs deadline, the last
          concrete error) when the search struggles or fails — for diagnosing a join that can't find
          a member

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet leave

```text
Exit a subnet: drop its membership and trust material from this node.

Removes the subnet's seed, its trust rows, its serve-state, and its registry snapshot here. Gated
behind OS elevation (membership exit destroys trust material). The remaining members still hold the
old seed — rotate it there if this machine should not rejoin.

Usage: spt subnet leave [OPTIONS] <NAME>

Arguments:
  <NAME>
          The held subnet to leave

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet prune

```text
Remove a dead node identity's trust rows (and registry rows).

The cleanup verb for a machine that re-paired under a new identity or is gone for good: its stale
trust rows cost a dial every pump tick. Takes a full pubkey hex, an unambiguous prefix, or a node
label. Gated behind OS elevation (trust mutation).

Usage: spt subnet prune [OPTIONS] <NODE>

Arguments:
  <NODE>
          The dead identity: pubkey hex, unambiguous prefix, or label

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet revoke

```text
Revoke node(s) fleet-wide and rotate the subnet seed.

The real revocation (vs prune's local cleanup): writes a PROPAGATING roster tombstone now — so
every member drops the node within a roster round — then schedules one seed rotation at the close of
a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch
bump). Benign offliners auto-heal across the rotation (re-seed grace); the revoked node is locked
out and must re-pair. Each target is a pubkey hex, an unambiguous prefix, or a label. Gated behind
OS elevation.

Usage: spt subnet revoke [OPTIONS] <NODES>...

Arguments:
  <NODES>...
          The identities to revoke: pubkey hex, unambiguous prefix, or label

Options:
      --force-rotate-seed
          Rotate the seed immediately instead of at the window's close — the compromised-node path
          (a benign offliner may then fall behind and must re-pair rather than re-seed)

      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet detach

```text
Stop serving a held subnet (the daemon keeps running).

The membership (seed) stays on disk, but this node neither advertises into nor connects to the
subnet — pairing responder, rendezvous meet, and registry gossip all skip it. Takes effect within
one pump cadence; spt subnet attach reverses it.

Usage: spt subnet detach [OPTIONS] <NAME>

Arguments:
  <NAME>
          The held subnet to stop serving

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --save
          Also persist as the startup default (survives daemon restarts)

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet attach

```text
Resume serving a detached subnet.

Advertising + connecting restart within one pump cadence.

Usage: spt subnet attach [OPTIONS] <NAME>

Arguments:
  <NAME>
          The held subnet to serve again

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --save
          Also persist as the startup default (survives daemon restarts)

  -h, --help
          Print help (see a summary with '-h')
```

### spt subnet notify

```text
Issue a subnet-wide user notification.

Produced into the replicated notification spool and first-fired at the user's most-recently-active
endpoint in that subnet. Body from the trailing arg, or stdin when omitted. Targets the calling
endpoint's HOME subnet unless --target names another (M8 decision 25: no resolvable home + no
--target = refuse).

Usage: spt subnet notify [OPTIONS] [BODY]

Arguments:
  [BODY]
          Notification body (read from stdin when omitted)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --target <TARGET>
          Target subnet (defaults to the calling endpoint's home subnet)

      --from <FROM>
          Issuer endpoint id (auto-detected from session if omitted)

  -h, --help
          Print help (see a summary with '-h')
```

## spt update

```text
Self-update: bare spt update brings the whole node current.

The bare form fetches + installs the latest core release, then updates every release-shipped adapter
— one command. The invoking session survives it: installing cycles only the daemon's coordinator
process, never the hosted terminals. apply is the explicit ack named by the update-consent
notification; it re-verifies the staged release before touching the live daemon.

Usage: spt update [OPTIONS]
       spt update <COMMAND>

Commands:
  apply     Apply the staged, verified self-update now
  fetch     Fetch the latest signed release from the GitHub origin and stage it (then spt update
            apply). Bootstraps a node with no peer to pull from
  adapters  Update release-shipped adapters (an alias of spt adapter update, which also stays).
            With no names, every release-shipped adapter is swept; with names (comma-separated),
            exactly those. Names are validated before anything updates, one adapter's failure never
            stops the rest, and a summary line reports each outcome
  help      Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -c, --core-only
          Update the core binary only — skip the adapters leg of the bare composite

      --restart
          The full-cycle form: fetch, update adapters, then finish by restarting the daemon onto the
          new version (update apply --finish) as the final step — so the whole node, coordinator
          and live agents, runs the new version when it returns. The restart bounces hosted sessions
          (they come back automatically)

  -h, --help
          Print help (see a summary with '-h')
```

### spt update apply

```text
Apply the staged, verified self-update now

Usage: spt update apply [OPTIONS]

Options:
      --finish  Finish onto the new version in one step: install it, then restart the daemon so both
                the coordinator and every live agent run the new version. Hosted sessions come back
                automatically — no manual restart. Without this flag, install alone leaves the
                running daemon on the previous version until you restart it yourself
      --json    Emit machine-readable JSON instead of the human view. Honored by the read/status
                commands (list, whoami, status, description, role, the *-list queries, how-to);
                action commands ignore it
  -h, --help    Print help
```

### spt update fetch

```text
Fetch the latest signed release from the GitHub origin and stage it (then spt update apply).
Bootstraps a node with no peer to pull from

Usage: spt update fetch [OPTIONS]

Options:
      --channel <CHANNEL>  Accept a release on this channel instead of the node's pin (e.g. beta).
                           Default: the node's pinned channel
      --json               Emit machine-readable JSON instead of the human view. Honored by the
                           read/status commands (list, whoami, status, description, role, the *-list
                           queries, how-to); action commands ignore it
      --tag <TAG>          Fetch a specific release tag (e.g. v0.3.1) instead of the latest
      --apply              Fetch then install in one step — apply the staged update even if the
                           latest was already downloaded. The one-shot "get me to the latest"
  -h, --help               Print help
```

### spt update adapters

```text
Update release-shipped adapters (an alias of spt adapter update, which also stays). With no names,
every release-shipped adapter is swept; with names (comma-separated), exactly those. Names are
validated before anything updates, one adapter's failure never stops the rest, and a summary line
reports each outcome

Usage: spt update adapters [OPTIONS] [NAMES]

Arguments:
  [NAMES]  Adapters to update, comma-separated (e.g. claude-spt,other). Omit to sweep every
           release-shipped adapter

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

## spt api

```text
Harness-contract inbound surface (hook entry points).

The entry points a harness's hooks fire to keep spt-core's on-disk state in sync.

Usage: spt api [OPTIONS] <COMMAND>

Commands:
  seed             Harness-hosted startup: record an ephemeral seed keyed by parent pid
  listen           Consume a seed and hold the perch + relay loop (blocks)
  bind             Post-spawn bind of a session to its perch
  bind-shell       Shell-binary bind: the type=Shell flavor of bind. Resolves the instance by
                   link token alone (the spawn template carries only {link_token} — "owner from
                   the link") and flips it online. The credential IS the auth: no token, no bind
  state            Set activity state busy|idle (also arms the echo-gate sentinel)
  echo-gate        Manage the echo-gate sentinel directly
  poll             Drain delivered messages (hook channel). With --link this is the shell-flavored
                   relay drain: the link token is the auth, and the drained rows are the shell's
                   MAC-stamped command/text/file frames
  psyche-download  Emit the agent's resume context (durable role/live/project tiers + any
                   not-yet-synthesized commune/signoff drop as pending slices) to stdout, for the
                   harness adapter's SessionStart hook to inject as additional context
  worker-start     Create a nested worker perch under a parent. The worker id is minted by spt-core
                   ({parent}-w{N}) and echoed as the bare id on stdout — the caller does NOT pass
                   one. --agent-id/--agent-type are optional correlation metadata, never the
                   perch identity
  worker-stop      Tear down a worker perch
  worker-poll      Drain a worker perch's messages
  boundary         Rebind the perch to a new session_id, preserving identity (a context
                   clear/compact boundary)
  session-end      Soft teardown (spool/history preserved); --erase hard-wipes
  presence         Report user/agent presence at this endpoint
  driven-by        Report which node (if any) is remote-driving this endpoint
  endpoint-info    Emit an endpoint's identity, where it runs, and which node (if any) is driving
                   it, as JSON. With no id, reports the caller's own endpoint (like whoami); an
                   explicit id reports that endpoint. Read-only
  history-log      Append normalized history (body on stdin) to the native history store
  digest-entry     Push one digest-record (the published contract JSON line, on stdin) for a
                   log-less adapter — appended to the perch's digest store, tailed by the
                   session-digest projection
  emit             Emit a Shell sensory payload to the owner's live session. REST-only by
                   definition: never spooled, dropped with a diagnostic when the owner isn't live.
                   The link token is the auth
  drive-poll       Take-and-clear a Shell's pending drive frame: the shell-side drain of the
                   owner→shell ephemeral control channel. REST-only, exactly-once — the daemon
                   serves the single latest frame and ONLY when the link matches the slot's
                   write-time stamp (no stale-control replay on relink). The link token is the auth
                   (mirrors emit). The frame, if any, prints to stdout
  tunnel           Use the shell end of the opaque byte TUNNEL: a held, reliable-ordered QUIC stream
                   the channel taxonomy never reinterprets (first consumer: USB/IP URB traffic).
                   send pipes raw stdin into the tunnel; recv drains buffered bytes to raw
                   stdout. The link token is the auth (mirrors drive-poll); the stream resolves
                   only under the live link generation. Poll-drained at the surface
  capability       Print the adapter's declared capability (hostable_types)
  hint             Keyword hints: the full user message arrives on stdin; emit at most one
                   matched hint line (declaration order, first unseen wins) for the adapter's
                   context channel. The per-session seen-set fires each hint once per --session (a
                   /clear = a new session = re-armed). Needs --manifest
  shutdown         Graceful live-agent signoff: run the final context save BEFORE teardown, then
                   soft-stop. The spt shutdown lifecycle path
  owner-shutdown   A shell suspends its linked owner directly, bypassing agent comms — gated by the
                   manifest can_shutdown pre-consent grant, fail-closed. The firing shell cascades
                   offline with its siblings, by design
  help             Print this message or the help of the given subcommand(s)

Options:
      --adapter <ADAPTER>
          adapter_name — the calling harness adapter. Optional: an explicit name[:profile]
          override for adapter dev/iteration. Omitted, listen resolves the owning adapter/profile
          at bind from the seed's parent pid (host_binaries → active-profile pointer →
          registered_at_ms)

      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --manifest <MANIFEST>
          Path to the adapter's runtime manifest (when the command needs it)

  -h, --help
          Print help (see a summary with '-h')
```

### spt api seed

```text
Harness-hosted startup: record an ephemeral seed keyed by parent pid

Usage: spt api seed [OPTIONS] --pid <PID> --session-id <SESSION_ID>

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --pid <PID>                
      --session-id <SESSION_ID>  
  -h, --help                     Print help
```

### spt api listen

```text
Consume a seed and hold the perch + relay loop (blocks)

Usage: spt api listen [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --parent-pid <PARENT_PID>  Override the parent-pid anchor (defaults to the self-discovered
                                 PPID)
      --once                     Drain backlog + one receive cycle, then exit (testability)
      --subnet <SUBNET>          Home subnet for a NEW endpoint (required on a multi-subnet node —
                                 home is assigned at creation, never guessed)
      --session-id <SESSION_ID>  Bind from this session id when the ephemeral seed is gone (a
                                 session going live late, or after a daemon restart). With no live
                                 seed and no session id, listen refuses (NO_SEED)
  -h, --help                     Print help
```

### spt api bind

```text
Post-spawn bind of a session to its perch

Usage: spt api bind [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                           Emit machine-readable JSON instead of the human view. Honored
                                       by the read/status commands (list, whoami, status,
                                       description, role, the *-list queries, how-to); action
                                       commands ignore it
      --set-session-id <BIND_SESSION>  The session id discovered post-spawn, written into the perch
                                       record
      --subnet <SUBNET>                Home subnet for a NEW endpoint (see listen)
      --type <ENDPOINT_TYPE>           The endpoint type tag (info.json state). Defaults to
                                       live_agent (the agent host); a non-agent endpoint — e.g. a
                                       gateway — binds with its own open-type tag. A revive keeps
                                       the prior type unless this overrides it [default: live_agent]
      --token <TOKEN>                  Capability token proving association to the target perch
      --session-id <SESSION_ID>        Session id proving association (matches the perch's
                                       info.json)
  -h, --help                           Print help
```

### spt api bind-shell

```text
Shell-binary bind: the type=Shell flavor of bind. Resolves the instance by link token alone
(the spawn template carries only {link_token} — "owner from the link") and flips it online. The
credential IS the auth: no token, no bind

Usage: spt api bind-shell [OPTIONS] --link <LINK_TOKEN>

Options:
      --json               Emit machine-readable JSON instead of the human view. Honored by the
                           read/status commands (list, whoami, status, description, role, the *-list
                           queries, how-to); action commands ignore it
      --link <LINK_TOKEN>  The link token the broker minted at launch
  -h, --help               Print help
```

### spt api state

```text
Set activity state busy|idle (also arms the echo-gate sentinel)

Usage: spt api state [OPTIONS] <STATE> <ID>

Arguments:
  <STATE>  [possible values: busy, idle]
  <ID>     

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --no-gate                  
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api echo-gate

```text
Manage the echo-gate sentinel directly

Usage: spt api echo-gate [OPTIONS] <ACTION> <ID>

Arguments:
  <ACTION>  [possible values: set, clear]
  <ID>      

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api poll

```text
Drain delivered messages (hook channel). With --link this is the shell-flavored relay drain: the
link token is the auth, and the drained rows are the shell's MAC-stamped command/text/file frames

Usage: spt api poll [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --include-deferred         
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --link <LINK>              Shell link token (the relay command-receipt drain)
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api psyche-download

```text
Emit the agent's resume context (durable role/live/project tiers + any not-yet-synthesized
commune/signoff drop as pending slices) to stdout, for the harness adapter's SessionStart hook to
inject as additional context

Usage: spt api psyche-download [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api worker-start

```text
Create a nested worker perch under a parent. The worker id is minted by spt-core ({parent}-w{N})
and echoed as the bare id on stdout — the caller does NOT pass one. --agent-id/--agent-type are
optional correlation metadata, never the perch identity

Usage: spt api worker-start [OPTIONS] <PARENT>

Arguments:
  <PARENT>  

Options:
      --agent-id <AGENT_ID>      Adapter's own agent id (correlation metadata only)
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --agent-type <AGENT_TYPE>  Adapter's own agent type (correlation metadata only)
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api worker-stop

```text
Tear down a worker perch

Usage: spt api worker-stop [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api worker-poll

```text
Drain a worker perch's messages

Usage: spt api worker-poll [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api boundary

```text
Rebind the perch to a new session_id, preserving identity (a context clear/compact boundary)

Usage: spt api boundary [OPTIONS] --to-session-id <TO_SESSION> <MODE> <ID>

Arguments:
  <MODE>  [possible values: clear, compact]
  <ID>    

Options:
      --json                        Emit machine-readable JSON instead of the human view. Honored by
                                    the read/status commands (list, whoami, status, description,
                                    role, the *-list queries, how-to); action commands ignore it
      --to-session-id <TO_SESSION>  The new session id to rebind the perch to
      --token <TOKEN>               Capability token proving association to the target perch
      --session-id <SESSION_ID>     Session id proving association (matches the perch's info.json)
  -h, --help                        Print help
```

### spt api session-end

```text
Soft teardown (spool/history preserved); --erase hard-wipes

Usage: spt api session-end [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --erase                    
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api presence

```text
Report user/agent presence at this endpoint

Usage: spt api presence [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api driven-by

```text
Report which node (if any) is remote-driving this endpoint

Usage: spt api driven-by [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api endpoint-info

```text
Emit an endpoint's identity, where it runs, and which node (if any) is driving it, as JSON. With no
id, reports the caller's own endpoint (like whoami); an explicit id reports that endpoint. Read-only

Usage: spt api endpoint-info [OPTIONS] [ID]

Arguments:
  [ID]  The endpoint id to report on. Omit to self-resolve the caller's perch

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt api history-log

```text
Append normalized history (body on stdin) to the native history store

Usage: spt api history-log [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api digest-entry

```text
Push one digest-record (the published contract JSON line, on stdin) for a log-less adapter —
appended to the perch's digest store, tailed by the session-digest projection

Usage: spt api digest-entry [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api emit

```text
Emit a Shell sensory payload to the owner's live session. REST-only by definition: never
spooled, dropped with a diagnostic when the owner isn't live. The link token is the auth

Usage: spt api emit [OPTIONS] --type <TYPE> --link <LINK> <ID> <PAYLOAD>

Arguments:
  <ID>       
  <PAYLOAD>  The sensory payload (descriptive text / encoded blob reference)

Options:
      --json         Emit machine-readable JSON instead of the human view. Honored by the
                     read/status commands (list, whoami, status, description, role, the *-list
                     queries, how-to); action commands ignore it
      --type <TYPE>  
      --link <LINK>  Shell link token (the per-link credential from launch)
  -h, --help         Print help
```

### spt api drive-poll

```text
Take-and-clear a Shell's pending drive frame: the shell-side drain of the owner→shell ephemeral
control channel. REST-only, exactly-once — the daemon serves the single latest frame and ONLY when
the link matches the slot's write-time stamp (no stale-control replay on relink). The link token is
the auth (mirrors emit). The frame, if any, prints to stdout

Usage: spt api drive-poll [OPTIONS] --link <LINK> <ID>

Arguments:
  <ID>  The shell instance id (must match the link token's instance)

Options:
      --json         Emit machine-readable JSON instead of the human view. Honored by the
                     read/status commands (list, whoami, status, description, role, the *-list
                     queries, how-to); action commands ignore it
      --link <LINK>  Shell link token (the per-link credential from launch)
  -h, --help         Print help
```

### spt api tunnel

```text
Use the shell end of the opaque byte TUNNEL: a held, reliable-ordered QUIC stream the channel
taxonomy never reinterprets (first consumer: USB/IP URB traffic). send pipes raw stdin into the
tunnel; recv drains buffered bytes to raw stdout. The link token is the auth (mirrors
drive-poll); the stream resolves only under the live link generation. Poll-drained at the surface

Usage: spt api tunnel [OPTIONS] --link <LINK> <ID> <DIRECTION>

Arguments:
  <ID>         The shell instance id (must match the link token's instance)
  <DIRECTION>  send (raw stdin → tunnel) or recv (tunnel → raw stdout)

Options:
      --json         Emit machine-readable JSON instead of the human view. Honored by the
                     read/status commands (list, whoami, status, description, role, the *-list
                     queries, how-to); action commands ignore it
      --link <LINK>  Shell link token (the per-link credential from launch)
  -h, --help         Print help
```

### spt api capability

```text
Print the adapter's declared capability (hostable_types)

Usage: spt api capability [OPTIONS]

Options:
      --json  Emit machine-readable JSON instead of the human view. Honored by the read/status
              commands (list, whoami, status, description, role, the *-list queries, how-to); action
              commands ignore it
  -h, --help  Print help
```

### spt api hint

```text
Keyword hints: the full user message arrives on stdin; emit at most one matched hint line
(declaration order, first unseen wins) for the adapter's context channel. The per-session seen-set
fires each hint once per --session (a /clear = a new session = re-armed). Needs --manifest

Usage: spt api hint [OPTIONS] --session <SESSION>

Options:
      --json               Emit machine-readable JSON instead of the human view. Honored by the
                           read/status commands (list, whoami, status, description, role, the *-list
                           queries, how-to); action commands ignore it
      --session <SESSION>  The harness session id keying the once-per-session seen-set
  -h, --help               Print help
```

### spt api shutdown

```text
Graceful live-agent signoff: run the final context save BEFORE teardown, then soft-stop. The `spt
shutdown` lifecycle path

Usage: spt api shutdown [OPTIONS] <ID>

Arguments:
  <ID>  

Options:
      --json                     Emit machine-readable JSON instead of the human view. Honored by
                                 the read/status commands (list, whoami, status, description, role,
                                 the *-list queries, how-to); action commands ignore it
      --token <TOKEN>            Capability token proving association to the target perch
      --session-id <SESSION_ID>  Session id proving association (matches the perch's info.json)
  -h, --help                     Print help
```

### spt api owner-shutdown

```text
A shell suspends its linked owner directly, bypassing agent comms — gated by the manifest
can_shutdown pre-consent grant, fail-closed. The firing shell cascades offline with its siblings,
by design

Usage: spt api owner-shutdown [OPTIONS] --link <LINK> <ID>

Arguments:
  <ID>  The shell instance id (must match the link token's instance)

Options:
      --json         Emit machine-readable JSON instead of the human view. Honored by the
                     read/status commands (list, whoami, status, description, role, the *-list
                     queries, how-to); action commands ignore it
      --link <LINK>  Shell link token (the per-link credential from launch)
  -h, --help         Print help
```

## spt endpoint

```text
Endpoint operations: list, lifecycle, fork, digest, access.

The noun home for per-endpoint verbs (M8 decision 1). Bare spt endpoint renders the merged listing
— every member subnet's endpoints grouped by subnet, this session's own endpoint pinned distinctly
at the top.

Usage: spt endpoint [OPTIONS] [COMMAND]

Commands:
  list         Merged endpoint listing (the bare spt endpoint view)
  run          Bring up an spt-hosted harness endpoint into a broker-held PTY
  fork         Fork an endpoint into another subnet as a NEW identity
  suspend      Rest an endpoint cold (the suspend edge)
  wake         Wake a resting endpoint in place
  shutdown     Gracefully shut down an agent's own endpoint
  stop         Soft-stop a perch (spool preserved)
  rename       Rename an endpoint's logical id across its on-disk state
  purge        Permanently remove an endpoint and every record keyed on it
  digest       Show a session's live activity buffer (session digest)
  access       Endpoint access whitelist for unsolicited off-node inbound
  description  The endpoint's service-description blurb (ex-resources)
  role         Show or set the endpoint's durable role — a broad statement of purpose stored in
               the mind (tracked/agents/<id>/live-role.md), which replicates with the agent and
               renders FIRST at start-transition context injection. Bare role prints the current
               role; --overwrite <file> replaces it from a file. This is the sole writer of
               the role — no automated path (reconcile / echo-commune / signoff) ever mutates it
  help         Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint list

```text
Merged endpoint listing (the bare spt endpoint view).

Every member subnet's endpoints grouped by subnet, with this session's own endpoint pinned at the
top, AND this node's local perches merged in (so a just-online endpoint not yet advertised still
shows — spt whoami is a thin alias and must see its own perch). --subnet filters the subnet view
to one subnet; --detail adds each endpoint's description blurb (the resource-registry yellow-pages
projection).

Usage: spt endpoint list [OPTIONS]

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --subnet <SUBNET>
          Limit the subnet view to one subnet

      --detail
          Add each endpoint's description blurb to the rows

      --show-all
          Also show suspended (resting) endpoints, which are hidden by default

      --workers
          Also show worker perches, which are hidden by default (they are process-local machinery
          under a parent agent, not standalone endpoints)

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint run

```text
Bring up an spt-hosted harness endpoint into a broker-held PTY.

Spawns the adapter's [session.self] command into a broker-owned PTY (the harness self-registers
its perch on bind), then starts / attaches / views per the terminal-action flag. The endpoint id
rides argv so the harness binds to exactly it. This is the non-interactive core (the interactive
picker lands in a later wave); the flags cover every terminal action so a spt-<id> shortcut can
bake a fully non-interactive launch.

Usage: spt endpoint run [OPTIONS]

Options:
      --adapter <ADAPTER>
          The harness adapter to host: <adapter>[:profile] (must be a registered kind="harness"
          adapter on this node). Omit (with --id) to launch the interactive picker

      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --id <ID>
          The endpoint id to bring up (charset: alphanumeric, -, _). Omit (with --adapter) to
          launch the interactive picker

      --create
          Mint a fresh session (the default; explicit so a non-interactive shortcut can bake
          create-vs-resume). Create promises a FRESH session: if a live one already exists the run
          refuses with ENDPOINT_CREATE_CONFLICT (exit 1) and creates nothing — attach with spt rc
          <id> or resume with --resume instead. Conflicts with --resume

      --resume <RESUME>
          Resume a prior session id instead of minting a fresh one

      --start
          Start the endpoint and return immediately (no attach)

      --attach
          Attach a local terminal after bringup (the default action)

      --view
          Attach read-only after bringup (watch; forward no input)

      --subnet <SUBNET>
          Home this endpoint to a named subnet. Required on a node that holds more than one subnet
          (the home is assigned at creation and is permanent); the sole subnet is used automatically
          when there is one

      --save
          Also save this run as a startup default: the daemon replays it (a fresh session, same
          adapter and working directory) every time it starts, until the entry is removed from
          daemon.json. One entry per endpoint id — a re-save replaces the prior one. A saved
          endpoint that fails to come up logs the failure and never blocks the daemon

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint fork

```text
Fork an endpoint into another subnet as a NEW identity.

Home subnets are immutable — fork is the cross-subnet move, never a re-home. Seeds the fork with a
one-time copy of the source's mind (live + project tiers); the two diverge immediately (no ongoing
sync). The source is untouched unless --delete-source. Same-node only today (one node holds one
perch per name, so a local fork needs a new id).

Usage: spt endpoint fork [OPTIONS] --subnet <SUBNET> <SRC> <NEW_ID>

Arguments:
  <SRC>
          The source endpoint (must exist on this node)

  <NEW_ID>
          The fork's id (must differ from the source on the same node)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --subnet <SUBNET>
          The fork's home subnet — the target (must be a member)

      --delete-source
          Delete the source endpoint (perch + tracked mind) after the copy

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint suspend

```text
Rest an endpoint cold (the suspend edge).

The resting state machine's suspend edge. From dormant — or straight from active, in which case the
final context save still fires first. Accepts a qualified id@node to suspend an instance on a
paired peer.

Usage: spt endpoint suspend [OPTIONS] <ID>

Arguments:
  <ID>
          The endpoint id (qualified id@node reaches a paired peer)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint wake

```text
Wake a resting endpoint in place.

Re-activates the existing seat (state's already there — no fresh spawn), resurfaces undismissed
notifications, and requests an immediate context freshness pull from trusted peers. Accepts a
qualified id@node for an instance on a paired peer.

Usage: spt endpoint wake [OPTIONS] <ID>

Arguments:
  <ID>
          The endpoint id (qualified id@node reaches a paired peer)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint shutdown

```text
Gracefully shut down an agent's own endpoint.

Soft-stops the listener, then the suspend edge — the final context save fires and persistent shells
cascade offline with it.

Usage: spt endpoint shutdown [OPTIONS] [ID]

Arguments:
  [ID]
          The endpoint id (defaults to the session's own perch)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint stop

```text
Soft-stop a perch (spool preserved).

Removes the ready marker and unregisters the perch; the spool is preserved.

Usage: spt endpoint stop [OPTIONS] <ID>

Arguments:
  <ID>
          Perch id to stop

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint rename

```text
Rename an endpoint's logical id across its on-disk state.

Rippled everywhere the id appears: the endpoint's perch dir, its nested companion/worker perches,
and every record naming it. Refuses while the perch is live (stop it first).

Usage: spt endpoint rename [OPTIONS] <OLD_ID> <NEW_ID>

Arguments:
  <OLD_ID>
          The endpoint's current (bare) id

  <NEW_ID>
          The new (bare) id — charset-validated; :/@ are reserved

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint purge

```text
Permanently remove an endpoint and every record keyed on it.

Deletes the perch tree (including its nested companion/worker perches and shells), the registry
address, the endpoint's context branches, and its node-local trust rows. Local only. Offline-only:
refuses while the endpoint is online — stop it first, or pass --force to stop-then-purge.
Irreversible; confirms interactively unless --yes.

Usage: spt endpoint purge [OPTIONS] <ID>

Arguments:
  <ID>
          The endpoint id to remove

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --yes
          Skip the interactive confirmation (for scripts / CI)

      --force
          Stop the endpoint first if it is online, then purge

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint digest

```text
Show a session's live activity buffer (session digest).

The at-a-glance "what is this agent doing now" view — a projection of the endpoint's normalized
session logs. Pulls a snapshot, or --follows the delta-stream. Local endpoints only.

Usage: spt endpoint digest [OPTIONS] <ID>

Arguments:
  <ID>
          The (local) endpoint id to read

Options:
      --follow
          Stream live changes instead of a one-shot snapshot (Ctrl-C to stop)

      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --last <LAST>
          Show the last N turns instead of the default window (--last 1 is the latest turn — the
          turn-end output)

      --after <AFTER>
          Cursor: show only entries newer than this seq (the authoritative dedup key from a prior
          pull). If the seq predates the window, the full window is returned with a predates signal

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint access

```text
Endpoint access whitelist for unsolicited off-node inbound.

Controls which origin nodes may send an endpoint unsolicited off-node inbound. Absent entry = open;
allow flips the endpoint to restricted; revoking the last node leaves it locked down; open
deletes the restriction.

Usage: spt endpoint access [OPTIONS] <COMMAND>

Commands:
  allow   Whitelist a node for an endpoint (creates the restriction if absent)
  revoke  Remove a node from an endpoint's whitelist. Never widens: revoking the last node leaves
          the endpoint locked down (all unsolicited refused)
  open    Delete an endpoint's restriction entirely — back to default-open
  list    List restrictions (all endpoints, or one)
  help    Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint description

```text
The endpoint's service-description blurb (ex-resources).

Bare description shows your own; set authors it. The cross-node projection over every visible
endpoint is endpoint list --detail.

Usage: spt endpoint description [OPTIONS] [COMMAND]

Commands:
  set   Author this endpoint's blurb (the agent refines its own at runtime; an empty string clears
        it back to the node-config seed)
  show  Show a local endpoint's authored blurb (the bare description view)
  help  Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt endpoint role

```text
Show or set the endpoint's durable role — a broad statement of purpose stored in the mind
(tracked/agents/<id>/live-role.md), which replicates with the agent and renders FIRST at
start-transition context injection. Bare role prints the current role; --overwrite <file>
replaces it from a file. This is the sole writer of the role — no automated path (reconcile /
echo-commune / signoff) ever mutates it

Usage: spt endpoint role [OPTIONS]

Options:
      --id <ID>                Which local endpoint (auto-detected from the session if omitted)
      --json                   Emit machine-readable JSON instead of the human view. Honored by the
                               read/status commands (list, whoami, status, description, role, the
                               *-list queries, how-to); action commands ignore it
      --overwrite <OVERWRITE>  Replace the role with the contents of <file> (the only writer)
  -h, --help                   Print help
```

## spt how-to

```text
Task-oriented instructions for agents: how-to <topic>.

The binary's own usage guidance, written for an agent to read and follow. Bare how-to lists the
topics.

Usage: spt how-to [OPTIONS] [TOPIC]

Arguments:
  [TOPIC]
          The topic to print (omit to list available topics)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

## spt ready

```text
Become reachable: register the perch and listen (blocks).

Drains the spooled backlog first; each received message prints to stdout. With --once, runs a single
drain+receive cycle and exits.

Usage: spt ready [OPTIONS] <ID>

Arguments:
  <ID>
          This agent's perch id

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --once
          Run a single drain+receive cycle, then exit (one-shot fallback for harnesses that cannot
          host a long-running listener)

      --subnet <SUBNET>
          Home subnet for a NEW endpoint (required on a multi-subnet node — home is assigned at
          creation, never guessed)

  -h, --help
          Print help (see a summary with '-h')
```

## spt ring

```text
Send and block for a reply (body read from stdin).

The reply body is printed to stdout; gives up after --timeout seconds.

Usage: spt ring [OPTIONS] <TARGET>

Arguments:
  <TARGET>
          Target perch id

Options:
      --from <FROM>
          Sender id (auto-detected from session if omitted)

      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

      --timeout <TIMEOUT>
          Seconds to wait for a reply before giving up
          
          [default: 60]

  -h, --help
          Print help (see a summary with '-h')
```

## spt send

```text
Send a message (body read from stdin); fire-and-forget

Usage: spt send [OPTIONS] <TARGET>

Arguments:
  <TARGET>  Target perch id

Options:
      --from <FROM>          Sender id carried structurally as the message from (auto-detected
                             from session if omitted)
      --json                 Emit machine-readable JSON instead of the human view. Honored by the
                             read/status commands (list, whoami, status, description, role, the
                             *-list queries, how-to); action commands ignore it
      --idle-only            Deliver only when the target is idle (the idle/wake window); hold until
                             then and never surface to the target's active poll
      --active-only          Deliver only through the target's own poll (the no-interrupt hook
                             channel); never wakes an idle target. Replaces the old --deferred
      --ephemeral            Drop the message if it cannot be delivered in its window, instead of
                             spooling until delivered
      --prefer-native        Deliver through the target's translation binary when one is running,
                             else fall back to the normal channel. Delivers regardless of
                             idle/active
      --force-native         Deliver ONLY through the target's translation binary — no fallback and
                             no spooling. If no binary is running the send is reported undelivered
      --json-payload <JSON>  Attach an opaque JSON metadata blob alongside the message body, carried
                             verbatim for the receiving adapter to parse. Does not replace the body
      --user-msg             Request the user-msg type (the user's authority). Honored only from a
                             user-backed origin (a Gateway endpoint, or the local user's own CLI);
                             an agent-family sender is re-stamped to plain msg
  -h, --help                 Print help
```

## spt shell

```text
Shell instances: mint, list, drive, tear down owned surfaces.

The driven surfaces this agent owns. spawn MINTS a new instance identity (<adapter>-<n>) — it is
not the online switch; bringing an existing offline instance back is relink / persistent / wake.

Usage: spt shell [OPTIONS] <COMMAND>

Commands:
  spawn     Mint a NEW shell instance of a registered kind="shell" adapter: canonical id
            <adapter>-<n> (smallest free n; teardown frees slots), starting offline (the launch +
            bind handshake brings it online)
  list      List this owner's instances: canonical id, alias, adapter, status
  teardown  Destroy an instance (perch removed; mint slot + alias freed)
  rename    Set/replace an instance's alias (owner-unique)
  cmd       Drive the shell with a typed capability command (the durable command channel): the op +
            positional args are vocabulary-checked against the manifest's [shell.capabilities],
            spooled on the shell perch, and drained by the manifest's command_receipt mode (relay
            / stdin)
  drive     Drive the shell with a typed, EPHEMERAL control payload: the owner→shell mirror of
            sensory. The drive-type is vocabulary-checked against [shell.drive], held in a single
            latest-wins in-memory slot on the daemon, and drained by the shell's api drive-poll
            --link. NEVER spooled — an offline shell drops the payload with a diagnostic (control
            is live-or-drop, never replayed)
  tunnel    Use the shell's opaque byte TUNNEL: a held, reliable-ordered QUIC stream the channel
            taxonomy never reinterprets (first consumer: USB/IP URB traffic). send pipes raw stdin
            bytes into the tunnel; recv drains buffered bytes to stdout. The shell opts in via
            [shell.tunnel]; the tunnel lives for the link (a link-break closes it). Poll-drained
            at the surface
  send      Send a text and/or file payload down the durable 2-way text+file channel (agent→shell;
            the shell answers via ordinary spt send). File transfers are progress-queryable by
            xfer id
  relink    Bring an existing offline (persistent) instance back online: re-spawns the binary with a
            fresh link token; the perch onlines at its bind
  help      Print this message or the help of the given subcommand(s)

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

### spt shell spawn

```text
Mint a NEW shell instance of a registered kind="shell" adapter: canonical id <adapter>-<n>
(smallest free n; teardown frees slots), starting offline (the launch + bind handshake brings it
online)

Usage: spt shell spawn [OPTIONS] <ADAPTER>

Arguments:
  <ADAPTER>  The providing shell adapter (must be registered + active)

Options:
      --alias <ALIAS>  Optional owner-unique friendly label (interchangeable with the canonical id
                       for addressing; never obscures the adapter)
      --json           Emit machine-readable JSON instead of the human view. Honored by the
                       read/status commands (list, whoami, status, description, role, the *-list
                       queries, how-to); action commands ignore it
      --owner <OWNER>  Owning endpoint id (auto-detected from session if omitted)
  -h, --help           Print help
```

### spt shell list

```text
List this owner's instances: canonical id, alias, adapter, status

Usage: spt shell list [OPTIONS]

Options:
      --json           Emit machine-readable JSON instead of the human view. Honored by the
                       read/status commands (list, whoami, status, description, role, the *-list
                       queries, how-to); action commands ignore it
      --owner <OWNER>  
  -h, --help           Print help
```

### spt shell teardown

```text
Destroy an instance (perch removed; mint slot + alias freed)

Usage: spt shell teardown [OPTIONS] <SHELL_REF>

Arguments:
  <SHELL_REF>  Canonical id or alias

Options:
      --json           Emit machine-readable JSON instead of the human view. Honored by the
                       read/status commands (list, whoami, status, description, role, the *-list
                       queries, how-to); action commands ignore it
      --owner <OWNER>  
  -h, --help           Print help
```

### spt shell rename

```text
Set/replace an instance's alias (owner-unique)

Usage: spt shell rename [OPTIONS] <SHELL_REF> <ALIAS>

Arguments:
  <SHELL_REF>  Canonical id or current alias
  <ALIAS>      The new alias

Options:
      --json           Emit machine-readable JSON instead of the human view. Honored by the
                       read/status commands (list, whoami, status, description, role, the *-list
                       queries, how-to); action commands ignore it
      --owner <OWNER>  
  -h, --help           Print help
```

### spt shell cmd

```text
Drive the shell with a typed capability command (the durable command channel): the op + positional
args are vocabulary-checked against the manifest's [shell.capabilities], spooled on the shell
perch, and drained by the manifest's command_receipt mode (relay / stdin)

Usage: spt shell cmd [OPTIONS] <SHELL_REF> [OP]...

Arguments:
  <SHELL_REF>  Canonical id or alias
  [OP]...      The capability op + args (vocabulary-checked against the manifest)

Options:
      --json           Emit machine-readable JSON instead of the human view. Honored by the
                       read/status commands (list, whoami, status, description, role, the *-list
                       queries, how-to); action commands ignore it
      --owner <OWNER>  
  -h, --help           Print help
```

### spt shell drive

```text
Drive the shell with a typed, EPHEMERAL control payload: the owner→shell mirror of sensory. The
drive-type is vocabulary-checked against [shell.drive], held in a single latest-wins in-memory
slot on the daemon, and drained by the shell's api drive-poll --link. NEVER spooled — an offline
shell drops the payload with a diagnostic (control is live-or-drop, never replayed)

Usage: spt shell drive [OPTIONS] --type <DRIVE_TYPE> <SHELL_REF> <PAYLOAD>

Arguments:
  <SHELL_REF>  Canonical id or alias
  <PAYLOAD>    The opaque control payload (descriptive text / encoded blob reference)

Options:
      --json               Emit machine-readable JSON instead of the human view. Honored by the
                           read/status commands (list, whoami, status, description, role, the *-list
                           queries, how-to); action commands ignore it
      --type <DRIVE_TYPE>  The drive payload type (vocabulary-checked against [shell.drive])
      --owner <OWNER>      
  -h, --help               Print help
```

### spt shell tunnel

```text
Use the shell's opaque byte TUNNEL: a held, reliable-ordered QUIC stream the channel taxonomy never
reinterprets (first consumer: USB/IP URB traffic). send pipes raw stdin bytes into the tunnel;
recv drains buffered bytes to stdout. The shell opts in via [shell.tunnel]; the tunnel lives for
the link (a link-break closes it). Poll-drained at the surface

Usage: spt shell tunnel [OPTIONS] <SHELL_REF> <DIRECTION>

Arguments:
  <SHELL_REF>  Canonical id or alias
  <DIRECTION>  send (raw stdin → tunnel) or recv (tunnel → raw stdout)

Options:
      --json           Emit machine-readable JSON instead of the human view. Honored by the
                       read/status commands (list, whoami, status, description, role, the *-list
                       queries, how-to); action commands ignore it
      --owner <OWNER>  
  -h, --help           Print help
```

### spt shell send

```text
Send a text and/or file payload down the durable 2-way text+file channel (agent→shell; the shell
answers via ordinary spt send). File transfers are progress-queryable by xfer id

Usage: spt shell send [OPTIONS] <SHELL_REF> [TEXT]

Arguments:
  <SHELL_REF>  Canonical id or alias
  [TEXT]       The text payload

Options:
      --file <FILE>    A file to transfer to the shell
      --json           Emit machine-readable JSON instead of the human view. Honored by the
                       read/status commands (list, whoami, status, description, role, the *-list
                       queries, how-to); action commands ignore it
      --owner <OWNER>  
  -h, --help           Print help
```

### spt shell relink

```text
Bring an existing offline (persistent) instance back online: re-spawns the binary with a fresh link
token; the perch onlines at its bind

Usage: spt shell relink [OPTIONS] <SHELL_REF>

Arguments:
  <SHELL_REF>  Canonical id or alias

Options:
      --json           Emit machine-readable JSON instead of the human view. Honored by the
                       read/status commands (list, whoami, status, description, role, the *-list
                       queries, how-to); action commands ignore it
      --owner <OWNER>  
  -h, --help           Print help
```

## spt whoami

```text
Who am I? This session's own endpoint, identity-only and fast.

Resolves the calling session to its endpoint ($OWL_SESSION_ID / $SPT_AGENT_ID / process ancestry)
and prints that one endpoint's SELF line — id, liveness, description. Never enumerates the roster,
never derives projects, never touches git or the network, so it answers in bounded time from hooks
and scripts under deadlines. For the full roster view use spt endpoint list.

Usage: spt whoami [OPTIONS]

Options:
      --json
          Emit machine-readable JSON instead of the human view. Honored by the read/status commands
          (list, whoami, status, description, role, the *-list queries, how-to); action commands
          ignore it

  -h, --help
          Print help (see a summary with '-h')
```

===== /reference/json-shapes.md =====

# JSON output shapes

<!-- [doc->REQ-DOC-DELIVERY-VOCAB] the machine-consumer reference: send-outcome vocabulary (canonical home cross-linked to Messaging), the endpoint-digest --json schema, the shell relay MAC-stamped frame prefix + api poll auth, and the full --json shapes catalog (seed #3) -->

Every read/status command takes a global **`--json`** flag and prints one
pretty JSON value to **stdout** (status lines stay on stderr — see the
[`api` output discipline](../harness-contract/api.md)). This page is the
machine-consumer's reference: the send-outcome vocabulary you classify by, the
session-digest schema, the shell relay's MAC-stamped frames, and the catalog
of `--json` shapes.

> A JSON value on **stdout**, a status tag on **stderr**, the truth in the
> **exit code**. A program reads whichever it needs and never has to scrape a
> human line.

## Send outcomes

The closed set of `spt send` outcome lines — `SENT`, `SENT(WAN)`, `QUEUED`,
`QUEUED(idle-only)`, `DEFERRED`, `NO_PERCH`, and the WAN failure tags — is
documented with its exact conditions in
[Messaging → Send outcomes](../messaging/overview.md#send-outcomes--the-closed-set).
The one rule a caller must encode: **classify by exit code** (`0` = every
delivered/spooled outcome, non-zero = every failure), and treat **`QUEUED` as
success** — the message is durably spooled and drains when the target next
comes online; never retry on it.

## Session digest — `endpoint digest --json`

`spt endpoint digest <id> --json` prints the endpoint's activity digest as a
projection of its session logs (not a PTY scrape). The top-level object:

```json
{ "turns": [ /* Turn, oldest → newest */ ] }
```

A `--after <seq>` cursor that predated the retained window adds one top-level
field, `"after_predates_window": true`, so a consumer knows it missed rows.

**`Turn`** — one user-opened turn:

| Field | Type | Notes |
|---|---|---|
| `input` | string \| null | The input that opened the turn; `null` for a preamble turn. |
| `entries` | array of entry | Agent/tool/boundary/context entries in stream order. |
| `input_seq` | number | Omitted when absent. |
| `partial` | bool | Omitted when false; `true` on the open trailing turn. |

**Entry** — an externally-tagged variant (the tag key names the kind):

| Variant | Fields |
|---|---|
| `Agent` | `text` (string); `seq`, `ts` optional |
| `ToolSprint` | `tools` (array of `{name, arg}`); `seq`, `ts` optional — consecutive tool uses collapse into one sprint |
| `Boundary` | `kind` ∈ `clear` \| `compact` \| `boot`; `ts` optional |
| `Context` | `kind` ∈ `psyche_download` \| `echo_mirror` \| `owl_message`; `body` (string); `ts` optional |

`ts` is an RFC3339-UTC ordering key. `--follow --json` streams per-update
deltas instead of the snapshot shape above.

> This is the digest **read** shape. It is distinct from the digest **record**
> an adapter *pushes* via `spt api digest-entry` / a `[digest]` extractor —
> that ingest contract (`role`/`text`/`tool`/`ts`) is documented in the
> [manifest digest-record reference](../harness-contract/manifest.md#session-digest--the-digest-record-contract).

## The shell relay — MAC-stamped frames

Two poll surfaces authenticate differently:

- **`spt api poll <id>`** — the agent hook-channel drain. Reads the caller's
  own perch spool; the manifest `[inject]` set must include the hook method.
  Each row prints as one whole `<EVENT …>` envelope on stdout.
- **`spt api poll <shell-id> --link <token>`** — the shell relay drain. The
  **link token is the credential**: it resolves the `(owner, shell)` pair and
  is refused (exit 1, `AUTH_REFUSED`) if no instance holds it. Rows are emitted
  **raw, one per line — deliberately not `<EVENT>`-wrapped** (the shell child
  parses its own vocabulary).

Shell-relay frames are **MAC-stamped**. The on-wire form is:

```text
<mac> <frame>
```

— a **64-hex-char HMAC-SHA256** over the frame bytes, one ASCII space, then the
raw frame. The key is `SHA-256(link_token)`; a frame with no valid MAC is
dropped, never processed. Agent-perch surfaces (`spt ready`, `api listen`,
`api poll <id>`) **never** emit stamped frames — a consumer of agent traffic
only ever sees `<EVENT>` / `<EVENT-PART>` lines.

## `--json` catalog

Commands that emit `--json`, and the top-level shape each prints. Fields marked
optional are omitted when empty.

| Command | Top-level shape |
|---|---|
| `endpoint list` | `{ self, subnets[], local[] }` — `self`: `{id, status, ready, alive, unbound, description, psyche_host_error, translation_fault?}`; `subnets[]`: `{name, endpoints[]}` where each endpoint is `{id, node, node_label, status, resources, endpoint_type?, project?}`; `local[]`: `{id, state, address, ready, alive, unbound, project?}`. *(Since v0.33.0 the local `project` field reads the daemon-maintained project index — answers are immediate and may lag a just-changed project by moments; absent while the index has never been built.)* |
| `whoami` | `{ id, state?, ready?, alive?, unbound?, description? }` — identity-only *(since v0.33.0; previously the `endpoint list` shape)*: the calling session's own endpoint, or `{"id": null}` + exit 1 when the session owns none. Never derives projects — the bounded-time identity verb for hooks. |
| `endpoint digest` | `{ turns[] }` — see [above](#session-digest--endpoint-digest---json) |
| `endpoint description show` | `{ id, description }` |
| `endpoint role` | `{ id, role }` |
| `api endpoint-info [<id>]` | `{ id, endpoint_type, adapter, local_node:{label,key}, attached_node:{label,key}\|null, controlled, project, cwd, subnets[] }` (always JSON) *(since v0.33.0 `project` is index-fed — bounded time, safe on hook paths)* |
| `daemon status` | `{ running, pid, net_up, pump_heartbeat_ms, managed_by, managed_active, subnets[], local_endpoints[], broker_image?, broker_stale?, stall_evict_count?, stall_evict_last_ms?, project_index? }` — `project_index` *(since v0.33.0)* is the index writer's health block: `{generated_ms, source_generation, pending_refresh, last_run_ms, last_duration_ms, last_error?, endpoints, projects, cwds, cwd_cache_hits, cwd_cache_misses, stale_reads, repairs, last_cycle:{branch_enumerations,tree_scans,derivations}, cumulative:{…}}`; absent when no writer has ever run on the home |
| `subnet status [--nodes]` | `{ daemon_running, subnets[] }` — each `{name, node_count, endpoint_count, nodes[]}` |
| `subnet show-code` | `{ subnet, code, otpauth_uri? }` |
| `notif list` | `{ notifs[] }` — each `{notif_id, subnet, kind, state, from_id, head}` |
| `access list` | `{ entries[] }` — each `{endpoint, nodes[], locked}` |
| `grant list` | `{ grants[] }` — each `{capability, agent, node, qualifier}` |
| `adapter list` | `{ adapters[] }` — each `{name, kind, mode, version, source_dir, active}` |
| `adapter version <option>` | `{ adapter, version }` |
| `shell list` | `{ owner, shells[], instantiable[] }` — each shell `{id, alias, adapter, status}` |

All shapes are **additive-forever**: new keys may appear, existing keys keep
their meaning. Parse tolerantly (ignore unknown fields) and a newer daemon
never breaks an older consumer.

===== /reference/schema.md =====

# Manifest JSON Schema

The machine-readable contract for adapter manifests, served at a stable URL:

**<https://sabermage.github.io/spt-releases/manifest.schema.json>**

- Generated from the **same code that parses manifests** — the schema is
  always exactly what `spt adapter add` accepts structurally. It also ships
  as a release asset with every release.
- JSON Schema draft 2020-12; the `$id` is the canonical URL above and is
  stable across releases.
- Field doc-comments ride along as `description`s — the schema doubles as
  field-level documentation.
- Manifests are authored as **TOML**; the schema describes the equivalent
  data model (validate the TOML-parsed document).
- Cross-field rules the schema can't express (kind↔`[shell]` agreement,
  strategy/avenue required fields) are listed in the
  [manifest reference](../harness-contract/manifest.md#cross-field-rules-spt-adapter-add-enforces-these)
  and enforced by `spt adapter add`.

Example — validate a manifest mechanically (Python, any JSON-Schema
validator works the same way):

```python
import json, tomllib, urllib.request, jsonschema

schema = json.load(urllib.request.urlopen(
    "https://sabermage.github.io/spt-releases/manifest.schema.json"))
with open("manifest.toml", "rb") as f:
    manifest = tomllib.load(f)
jsonschema.validate(manifest, schema)   # raises on violation
print("manifest is structurally valid")
```

===== /reference/install.md =====

# Installing

<!-- [doc->REQ-INSTALL-BOOTSTRAP-VERB] -->
Installation rides the GitHub CLI (`gh`) and a self-install verb built into
the binary itself *(since v0.32.0 — the hosted one-liner scripts are
retired)*. The release channel is a private GitHub repository, so each node
authenticates with an account that can read it; there is nothing else to
trust on first fetch beyond gh's authenticated TLS.

## Steps

1. **Install gh** (once per machine):

   - Windows: `winget install --id GitHub.cli`
   - macOS: `brew install gh`
   - Linux: `sudo apt install gh` (or your distro's package manager)

2. **Authenticate** with an account that can read the release channel:

   ```sh
   gh auth login
   ```

3. **Download the platform binary** from the release channel:

   ```sh
   gh release download --repo BigscreenVR/spt-bs-releases --pattern 'spt-x86_64-linux'       # Linux (glibc)
   gh release download --repo BigscreenVR/spt-bs-releases --pattern 'spt-x86_64-windows.exe' # Windows
   ```

   The published assets are `spt-x86_64-windows.exe`, `spt-x86_64-linux`
   (glibc), and `spt-x86_64-linux-musl`. The downloaded file keeps its
   `spt-*` name — don't rename it.

4. **Run the self-install verb** from the downloaded binary (on Linux
   `chmod +x` first — `gh` does not set the exec bit):

   ```sh
   chmod +x ./spt-x86_64-linux && ./spt-x86_64-linux install   # Linux
   .\spt-x86_64-windows.exe install                            # Windows
   ```

The verb places the binary at the canonical install path (the spt home's
`bin` dir), registers that directory on your **user** PATH (at most once —
re-running is always safe), and refuses a binary built for another platform.
It is non-interactive by construction. First-run identity generation and
daemon start happen on the first normal `spt` invocation, exactly as before.

The PATH change reaches **new** terminals only; the verb prints the absolute
installed path for use in the current one.

## Flags

| Flag | Meaning |
|---|---|
| `--dir <path>` | Override the install directory |
| `--no-path` | Skip user-PATH registration |

## Trust model

First fetch: gh's authenticated TLS + release-channel access control.
Thereafter `spt update` performs full Ed25519 verification against the
[two-key trust anchor](../self-update/overview.md#the-trust-chain) embedded
in the binary — the update carrier is also gh, and the signature chain is
carrier-independent.

## OS-service registration

Not yet: the daemon auto-starts on any `spt` invocation, which covers
dev-stage use. Known gap until then: after a reboot, a node is unreachable
until something on it invokes `spt`. Service registration ships in a later
release.