# spt-core — full docs export # Generated: concatenation of every page of https://sabermage.github.io/spt-releases 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 One line, non-interactive: ```sh # Linux curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh ``` ```powershell # Windows (PowerShell) irm https://sabermage.github.io/spt-releases/install.ps1 | iex ``` Verify: ```console $ spt --version spt 0.1.0 ``` ## 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. ## For AI agents reading this - [`llms.txt`](https://sabermage.github.io/spt-releases/llms.txt) — curated index of these docs. [`llms-full.txt`](https://sabermage.github.io/spt-releases/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`](https://sabermage.github.io/spt-releases/manifest.schema.json) — the machine-readable adapter-manifest contract. Validate your manifest against it before registering. - `spt --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 End to end in under 10 minutes. The roles matter here: **you** install (and optionally pair machines); **your agents** exchange the messages. You hand each agent a short prompt; the binary itself teaches them the rest. > This is the developer path. Building an adapter or integrating a harness? > Go to the [adapter quickstart](adapter.md) instead. Everything below uses real values and runs as written. ## 1. Install (one line) ```sh # Linux curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh ``` ```powershell # Windows (PowerShell) irm https://sabermage.github.io/spt-releases/install.ps1 | iex ``` Verify (on Windows, open a **new** terminal first — or use the absolute path the installer printed): ```console $ spt --version spt 0.1.0 ``` ## 2. Optional: link two machines Everything below also works on a single box — skip ahead freely. But the product's hallmark is that the *same* commands work across machines once they share a **subnet**.
Pair a second machine into a subnet (one-time, ~2 minutes) 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.
## 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 hello sergey - lea here ``` `SENT` means live delivery — sergey was listening. Each delivery is one `` 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 `
`; oversized deliveries split into `` lines the receiver concatenates back. ## 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 ping while you were away ``` The backlog drains the moment he's back (`--once` drains and exits — the one-shot form `spt how-to ready` teaches agents whose harness can't host a long-running listener). Nothing is lost between sessions. ## 6. What just happened - **Perch** — registering as `sergey` created a perch: a durable identity with an address and a spool, under spt-core's per-machine home. `spt list` shows every perch on the node, live or not. - **Live-first, spool-fallback** — `send` tries a direct connection to the registered address first (`SENT`); if the perch exists but no listener is up, the message lands in the spool (`QUEUED`) and is drained by the next `ready`. - **Reply routing** — the sender id travels with every message structurally, surfaced as the arriving `` 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 `: 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 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-one-line): ```sh curl -fsSL -o mock-adapter.zip \ https://github.com/SaberMage/spt-releases/releases/latest/download/mock-adapter.zip unzip mock-adapter.zip -d mock-adapter ``` (Windows: `irm -OutFile mock-adapter.zip https://github.com/SaberMage/spt-releases/releases/latest/download/mock-adapter.zip` 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`](https://sabermage.github.io/spt-releases/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 ` — 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/SaberMage/spt-releases#license). ## 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 ` delivers live when the target is listening, spools when it isn't; `spt ring ` 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 ` 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 `` envelope surfaces it, and `spt send ` answers without knowing anything else. - **The blocking ask.** `spt ring ` 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 ''` 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. ## 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 ` 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 hollow `UNBOUND` row, distinct from 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 `-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 process, spawned/resumed from the adapter's `psyche_init`/`psyche_resume` templates. ## 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. ## 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 ` 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 ` reads the last N turns, every entry carries a stable `seq`, and `--after ` returns only what is newer (the trailing in-progress turn is flagged `partial`). 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 ` finds a member over LAN + relay and runs the exchange. The code bootstraps a PAKE key exchange — the code is never the key, and a wrong guess learns nothing. Both sides pin each other's node keys on success (trust-on-first-use; key changes warn and never auto-apply). Every member machine answers join attempts automatically — no arming step on the existing fleet. - **Elevation gates** — `subnet create` (reveals a fresh subnet's joining secret) and `subnet join` (enrolls the whole machine) require an elevated terminal; `subnet status` is read-only and ungated, and never prints secrets. - **Visibility & sync scope** — per endpoint, per subnet: an endpoint can be hidden from a subnet (neither advertised nor routable) and its mind syncs only to subnets on its membership list. Both default conservative; unconfigured means *not shared*. - **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 ` — 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. ## 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`](https://sabermage.github.io/spt-releases/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 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 `** | 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 ` stays available as an optional override | REGISTER | | **Startup pair — pick one flow:**
• harness-hosted: `[hooks.SessionStart] → api seed --pid {parent_pid} --session-id {session_id}` then the session's `api listen `
• spt-hosted: `[session.self]` template (spt-core spawns it) then `api bind --set-session-id ` | 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 `** (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 --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 --to-session-id `** | The endpoint's identity, spool, and history survive a context reset under a new session id | BOUNDARY | | **`api psyche-download `** (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 `` 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 the same `spt adapter update` — pull the `.spt` **and** run an in-harness sync from one lever. 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-isolated (warn + fall back, never rolls back the pull) | 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 ` 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 `** | Graceful signoff — runs the final echo-commune BEFORE teardown so the context delta is never lost to ordering | END | | **`api presence ` / `api driven-by `** | Most-recently-active resolution across the subnet; lets a session tell local input from remote-drive | RUN | | **Workers** (`api worker-start `, `worker-poll`, `worker-stop`) | 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 `` 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 `-` (the `spt endpoint run` `s` keybind) — your harness's brand instead of the `spt-` 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-` (vs the `spt-` 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 `-commune.md` (delta context) or `-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 ` 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 `-commune.md` at every `/clear` and `/compact`, and a Self-authored `-signoff.md` at graceful stop, into the watched `commune_dir`; the `[session.psyche_init]` / `[session.psyche_resume]` / `[session.echo_commune]` templates let spt-core spawn 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.]` 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 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` → `/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 --json` supports turn-end incremental consumption (v0.16.0): `--last ` (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 ` (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. **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. Per-command output shapes are in the [CLI reference](../cli/reference.md). --- ## "Am I done?" — the floor - [ ] Manifest validates against [`manifest.schema.json`](https://sabermage.github.io/spt-releases/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 ` 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`](https://sabermage.github.io/spt-releases/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 `-commune.md` / `-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. | | `{adapter_name}` | The adapter's declared `name` (the value every `api` call carries). | | `{session_id}` | The harness session id (minted at spawn; reported back via `api seed`). | | `{session_name}` | The session's display name, when one is supplied. | | `{parent_pid}` | The harness parent process pid — the SessionStart `api seed` anchor. | | `{agent_type}` | The hosted agent type. | | `{agents_json}` | The resolved agents roster for the session. | | `{psyche_dir}` | A live agent's Psyche perch directory (its working dir). | | `{psyche_prompt}` | The composed Psyche turn prompt for this spawn. | | `{psyche_context}` | The Psyche's carried context block. | | `{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 ` 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.` (a self-update can rename the running exe); a declared name must not contain a dot | ## `[hooks.]` — 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 -commune.md signoff_dir = ".my-harness" # watched for -signoff.md ``` Commune and signoff are **file-drops, not commands** — an agent writes a markdown file; spt-core's watcher does the rest. ### `[session.]` — 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` / `psyche_resume` (the endpoint's persistent-context companion) · `echo_commune` (the bounded history summarizer for sessions that end without a signoff) · `signoff` (final context save) · `notif` (endpoint-native notification render). **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 `, 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 [session.psyche_init] command = "my-harness run --agent psyche --prompt {psyche_prompt} --model cheap" cwd = "{psyche_dir}" env_remove = ["MY_HARNESS_SESSION_ID"] recursion_guard_env = "SPT_ECHO_COMMUNE" detach = true keys = ["psyche_prompt", "psyche_dir"] ``` For the Psyche roles (`psyche_init` / `psyche_resume`) spt-core fills exactly `{id}` (the nested `-psyche` id), `{session_id}`, `{psyche_dir}`, and `{psyche_prompt}` — **not** `{session_name}` (that key is a `[session.self]` fill). Declaring a key your role's spawn isn't given fails at spawn, so a `psyche_init` template must template only those four. **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 `/` (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_init]` runner, 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.]` — 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). spt-core fills `{source}` with the resolved path and pipes the bytes on stdin. | | `source` | no | Own-source log path; absent, reuse `[history].locate_template`. One of the two **must** resolve, else `spt adapter add` rejects (see [Cross-field rules](#cross-field-rules-spt-adapter-add-enforces-these)). | | `window_turns` / `arg_truncation` / `sprint_collapse` | no | Adapter-declared presentation **defaults**; any consumer may override. spt-core fallback: `3` / `25` / collapse-on. | 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 --sample `. `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 ` 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 **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 `` 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":""}` per inbound message (the `` 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":""}` · `{"key":"enter"}` · `{"commit":true}`, … (extensible vocabulary). - **`{"commit":true}` is the mandatory sequence terminator.** 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. If no `{"commit":true}` arrives within the **commit deadline (5 s)**, spt-core faults the sequence and falls back to a raw inject — buffered operator input is still flushed, but the delivery is degraded. - 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. - The raw `payload + \r` inject is the **degenerate** case: a binary that emits `{"text":payload}{"key":"enter"}{"commit":true}` with no choreography. ## `[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 `) 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 ` — 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//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//{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 ` 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 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 version_check = true # check min_spt_core_version before/after 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 `.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. ## 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 ` (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`, **and** 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`"*) — 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. Two rules apply to `api` calls: 1. **`--adapter ` 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 `:` 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 runs the Psyche companion); 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 ` (matching the perch's record) or a capability `--token`; shell commands authenticate with `--link ` (the link token minted at launch *is* the credential — no token, no access). ```text spt api [--adapter ] [--manifest ] … ``` `--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 (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 --set-session-id ``` No seed file is involved; `bind` attaches the live session to its perch post-spawn. ## Session lifecycle ### `api seed --pid --session-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:`. ### `api listen [--once] [--parent-pid ] [--subnet ]` 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). ### `api bind [--set-session-id ]` 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: 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 ` for spt-core to match against the record this bind wrote. ### `api boundary --to-session-id ` 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. ### `api psyche-download [--session-id ]` 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 `` / `` 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:` 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 [--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 ` 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 [--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 ` 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 ` Report user/agent presence at this endpoint (feeds most-recently-active resolution across the subnet). ### `api driven-by ` 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 [--include-deferred] [--link ]` 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. ### `api history-log ` 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: - `api worker-start ` — create a nested worker perch. - `api worker-poll ` — drain the worker's messages. - `api worker-stop ` — tear the worker perch down. ## 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 ` 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 --type --link ` 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 --link ` 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. ## Conventions - **Output is line-oriented and stable**: `SEEDED:`, `READY:`, `SENT:`, `QUEUED:`, 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 `-commune.md` / `-signoff.md` into the manifest's watched directory; spt-core's watcher ingests it. There is deliberately no `api commune`. ===== /harness-contract/install-on-demand.md ===== # Install-on-demand bootstrap How an adapter ships spt-core *with itself*. The contract: **the canonical install one-liner 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 runs the official script when it's missing. The scripts are non-interactive **by construction** (they run unattended), idempotent (safe to re-run), sha256-verify what they download, and register the user PATH. Served from the permanent canonical URL: - `https://sabermage.github.io/spt-releases/install.sh` - `https://sabermage.github.io/spt-releases/install.ps1` ## The generic contract ```text if `spt` is on PATH -> done (optionally check `spt --version` ≥ your floor) else -> run the official one-liner for the OS then -> first invocation may need the absolute path (Windows) then -> register your manifest: spt adapter add --github / ``` After first install, spt-core keeps itself current (signed self-update), 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..." curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh # current shell may not see the PATH update yet: SPT="$HOME/.local/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..." irm https://sabermage.github.io/spt-releases/install.ps1 | iex # 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 / # latest "$SPT" adapter add --release / --tag v1.0.0 # pinned # ...or clone a repo whose ROOT holds manifest.toml: "$SPT" adapter add --github / # ...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 HTTPS + GitHub exactly like the install one-liner'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 `/` 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 `/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//` 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_init]` runner, 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 installer 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 installer — 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 installer 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: `$HOME/.local/bin/spt` absolutely, once. ## Pinning and air-gapped installs The scripts are configured by environment knobs, so the pipe-to-shell form stays canonical: | Env var | Meaning | |---|---| | `SPT_INSTALL_VERSION` | Install a specific release tag instead of latest | | `SPT_INSTALL_DIR` | Override the install directory | | `SPT_INSTALL_ASSET_BASE` | A URL or local directory holding the release assets + `SHA256SUMS` directly (CI, air-gap, mirrors) | | `SPT_INSTALL_NO_PATH` | `1` = skip PATH registration | Example — pin a version inside a CI job: ```sh SPT_INSTALL_VERSION=v0.1.0 \ curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh ``` ## Trust model First fetch trusts HTTPS + GitHub and verifies the binary's sha256 against the release's `SHA256SUMS`. From then on, `spt update` performs full Ed25519 signature verification against the two-key trust anchor embedded in every binary — so the installer 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 `** 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 `:` and **leaf-replaces only the leaves you declare** — everything else inherits from base. Override exactly what differs: - `[profiles..session.self].command` — retarget the bringup command (for example, wrap the launch in another binary). - `[profiles..digest].` — widen one digest knob. - `[profiles..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 : ` 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 ` 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 `body` (the live listener stream uses the same shape). Multi-message drains split cleanly on ``. Decode a body by splitting on `
` → newline, then HTML-unescaping `< > "` and `&` **last**. 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 `` 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 `/` 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 ` 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:`). - For any verb, **`spt --help` is the always-present source-of-truth** — it tracks the shipped binary. A skill body that says "the verb list is `spt --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 ``'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 --sample ` (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.]` with `direction = "inject"`, `value = "{id}"`); the start hook reads that env and self-registers with `api bind `. That bind is **intrinsically authenticated**: for a broker-spawned session the broker parentage is the proof, so `api bind --set-session-id ` 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 (`-`) 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 :`](../cli/reference.md)) or an explicit `--adapter :` override on the `listen` call. - `psyche_init` fills exactly four keys: `{id, session_id, psyche_dir, psyche_prompt}`. spt-core **overrides** `{id}` to `-psyche` before substitution, so the companion gets its own derived perch id. (The resume/preload key `{psyche_context}` is a separate seam; a first spawn fills it on resume.) - The companion is launched **detached and fire-and-forget**: `detach = true`, `cwd = "{psyche_dir}"`, stdio null, handle dropped, **unsupervised**. Liveness stays daemon-authoritative through the companion's perch. It owns the `-psyche` perch, communicates over its perch and commune file-drops, and exits at session end. - **The companion runner is yours to build; its lifecycle is the daemon's.** `psyche_init.command` is adapter-authored and opaque to spt-core. Declare the seam and build the runner, and let the daemon own spawn and teardown (a graceful `endpoint shutdown` tears the companion down with the perch). - **If your harness's headless mode runs one turn per invocation**, make the runner a small **resident wrapper** so it persists across pulses: seed the companion once from `{psyche_prompt}`, then drive one resume-turn per perch pulse, with the companion authoring commune drops. Build it like the `[digest]` extractor — a compiled, dependency-light binary the daemon can exec bare on any platform. ### Prove live bringup non-interactively To prove your live path actually brings a companion up — 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 --session-id ` (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 ` `QUEUED`s against it, ready to drain on bringup. 3. **Spawn the persistent relay as a child**, capturing its stdout/stderr: `spt api listen ` (no `--once` — that exits after one delivery). The adapter resolves from your `[adapter] host_binaries`; pass `--adapter --manifest ` only to pin a specific adapter/profile. Assert `BOUND:` then `READY:` on its stderr, and the relayed `` carrying your probe on its stdout. 4. **Assert the companion came up.** The relay marks the perch online; the **daemon** hosts the Psyche off that status — liveness is daemon-authoritative, the relay process does not spawn it. So assert the companion's `-psyche` perch comes online and the endpoint reports kind `live_agent`; with a live daemon the host marker `LIVEHOST_PSYCHE:` is on the **daemon's** stderr, not the relay child's. 5. **Kill the child** to end the session — the relay is freely killable; the Psyche lifecycle is the daemon's (a graceful `spt endpoint shutdown ` tears it down with the perch). 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 `-commune.md` (delta context) or `-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 `-ci-`, 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 --manifest 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 --sample ` 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 --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. - **Proof a DEV build off disk — `--dir` / `--manifest`.** Both `digest-proof` and `translate-proof` accept `--dir ` (binaries resolve there, just like a registered install) or `--manifest ` (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 `/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. ## Commands `spt endpoint list` · `endpoint rename` · `endpoint fork` · `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 ` 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.]` 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 ` to come online, then loops `api poll --link ` 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 --link ` — 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 `. 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. ## 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. After self-updating, spt-core **ripple-updates registered adapters** through each manifest's declared `[update]` avenue. ### 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 the same `spt adapter update`. 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); - 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** — if it fails, spt-core warns loudly and falls back to the today behavior (an applied update fires `[update].message`); a committed pull is never rolled back. The exact stdin keys, sentinel, and notice precedence are in the [manifest `[update.post]` reference](../harness-contract/manifest.md#update--adapter-self-update). ## 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: https://sabermage.github.io/spt-releases 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 grant Consent grant store: gated capabilities held on this node help Print this message or the help of the given subcommand(s) 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 operations 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 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 Print this session's own 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 -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] 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 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 ([:profile] ). 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 (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 (:). 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 [:profile] a harness session binds to when no --adapter is given. spt adapter use [:profile] points every host binary the adapter declares at it (run once per host binary you support); --clear 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 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 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 Release tag for --release (default: the latest release) --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] Arguments: 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]