URL: https://sabermage.github.io/spt-releases/harness-contract/api.html
Content-Type: text/html
Method: native

---

---
meta-description: spt-core: a harness-independent core for an agent ecosystem — messaging, live-agent lifecycle, terminal hosting, P2P networking, and the runtime-manifest harness contract.
meta-theme-color: #ffffff
meta-viewport: width=device-width, initial-scale=1
title: The spt api surface - SPT developer docs
---

## Keyboard shortcuts

Press `←` or `→` to navigate between chapters

Press `S` or `/` to search in the book

Press `?` to show this help

Press `Esc` to hide this help

![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48cGF0aCBkPSJNMCA5NkMwIDc4LjMgMTQuMyA2NCAzMiA2NEg0MTZjMTcuNyAwIDMyIDE0LjMgMzIgMzJzLTE0LjMgMzItMzIgMzJIMzJDMTQuMyAxMjggMCAxMTMuNyAwIDk2ek0wIDI1NmMwLTE3LjcgMTQuMy0zMiAzMi0zMkg0MTZjMTcuNyAwIDMyIDE0LjMgMzIgMzJzLTE0LjMgMzItMzIgMzJIMzJjLTE3LjcgMC0zMi0xNC4zLTMyLTMyek00NDggNDE2YzAgMTcuNy0xNC4zIDMyLTMyIDMySDMyYy0xNy43IDAtMzItMTQuMy0zMi0zMnMxNC4zLTMyIDMyLTMySDQxNmMxNy43IDAgMzIgMTQuMyAzMiAzMnoiIC8+PC9zdmc+)

![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIj48cGF0aCBkPSJNMzcxLjMgMzY3LjFjMjcuMy0zLjkgNTEuOS0xOS40IDY3LjItNDIuOUw2MDAuMiA3NC4xYzEyLjYtMTkuNSA5LjQtNDUuMy03LjYtNjEuMlM1NDkuNy00LjQgNTMxLjEgOS42TDI5NC40IDE4Ny4yYy0yNCAxOC0zOC4yIDQ2LjEtMzguNCA3Ni4xTDM3MS4zIDM2Ny4xem0tMTkuNiAyNS40bC0xMTYtMTA0LjRDMTc1LjkgMjkwLjMgMTI4IDMzOS42IDEyOCA0MDBjMCAzLjkgLjIgNy44IC42IDExLjZjMS44IDE3LjUtMTAuMiAzNi40LTI3LjggMzYuNEg5NmMtMTcuNyAwLTMyIDE0LjMtMzIgMzJzMTQuMyAzMiAzMiAzMkgyNDBjNjEuOSAwIDExMi01MC4xIDExMi0xMTJjMC0yLjUtLjEtNS0uMi03LjV6IiAvPjwvc3ZnPg==)

- Auto

- Light

- Rust

- Coal

- Navy

- Ayu

![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNNDE2IDIwOGMwIDQ1LjktMTQuOSA4OC4zLTQwIDEyMi43TDUwMi42IDQ1Ny40YzEyLjUgMTIuNSAxMi41IDMyLjggMCA0NS4zcy0zMi44IDEyLjUtNDUuMyAwTDMzMC43IDM3NmMtMzQuNCAyNS4yLTc2LjggNDAtMTIyLjcgNDBDOTMuMSA0MTYgMCAzMjIuOSAwIDIwOFM5My4xIDAgMjA4IDBTNDE2IDkzLjEgNDE2IDIwOHpNMjA4IDM1MmM3OS41IDAgMTQ0LTY0LjUgMTQ0LTE0NHMtNjQuNS0xNDQtMTQ0LTE0NFM2NCAxMjguNSA2NCAyMDhzNjQuNSAxNDQgMTQ0IDE0NHoiIC8+PC9zdmc+)

# SPT developer docs

[![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMTI4IDBDOTIuNyAwIDY0IDI4LjcgNjQgNjR2OTZoNjRWNjRIMzU0LjdMMzg0IDkzLjNWMTYwaDY0VjkzLjNjMC0xNy02LjctMzMuMy0xOC43LTQ1LjNMNDAwIDE4LjdDMzg4IDYuNyAzNzEuNyAwIDM1NC43IDBIMTI4ek0zODQgMzUydjMyIDY0SDEyOFYzODQgMzY4IDM1MkgzODR6bTY0IDMyaDMyYzE3LjcgMCAzMi0xNC4zIDMyLTMyVjI1NmMwLTM1LjMtMjguNy02NC02NC02NEg2NGMtMzUuMyAwLTY0IDI4LjctNjQgNjR2OTZjMCAxNy43IDE0LjMgMzIgMzIgMzJINjR2NjRjMCAzNS4zIDI4LjcgNjQgNjQgNjRIMzg0YzM1LjMgMCA2NC0yOC43IDY0LTY0VjM4NHptLTE2LTg4Yy0xMy4zIDAtMjQtMTAuNy0yNC0yNHMxMC43LTI0IDI0LTI0czI0IDEwLjcgMjQgMjRzLTEwLjcgMjQtMjQgMjR6IiAvPjwvc3ZnPg==)](../print.html "Print this book") [![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OTYgNTEyIj48cGF0aCBkPSJNMTY1LjkgMzk3LjRjMCAyLTIuMyAzLjYtNS4yIDMuNi0zLjMuMy01LjYtMS4zLTUuNi0zLjYgMC0yIDIuMy0zLjYgNS4yLTMuNiAzLS4zIDUuNiAxLjMgNS42IDMuNnptLTMxLjEtNC41Yy0uNyAyIDEuMyA0LjMgNC4zIDQuOSAyLjYgMSA1LjYgMCA2LjItMnMtMS4zLTQuMy00LjMtNS4yYy0yLjYtLjctNS41LjMtNi4yIDIuM3ptNDQuMi0xLjdjLTIuOS43LTQuOSAyLjYtNC42IDQuOS4zIDIgMi45IDMuMyA1LjkgMi42IDIuOS0uNyA0LjktMi42IDQuNi00LjYtLjMtMS45LTMtMy4yLTUuOS0yLjl6TTI0NC44IDhDMTA2LjEgOCAwIDExMy4zIDAgMjUyYzAgMTEwLjkgNjkuOCAyMDUuOCAxNjkuNSAyMzkuMiAxMi44IDIuMyAxNy4zLTUuNiAxNy4zLTEyLjEgMC02LjItLjMtNDAuNC0uMy02MS40IDAgMC03MCAxNS04NC43LTI5LjggMCAwLTExLjQtMjkuMS0yNy44LTM2LjYgMCAwLTIyLjktMTUuNyAxLjYtMTUuNCAwIDAgMjQuOSAyIDM4LjYgMjUuOCAyMS45IDM4LjYgNTguNiAyNy41IDcyLjkgMjAuOSAyLjMtMTYgOC44LTI3LjEgMTYtMzMuNy01NS45LTYuMi0xMTIuMy0xNC4zLTExMi4zLTExMC41IDAtMjcuNSA3LjYtNDEuMyAyMy42LTU4LjktMi42LTYuNS0xMS4xLTMzLjMgMi42LTY3LjkgMjAuOS02LjUgNjkgMjcgNjkgMjcgMjAtNS42IDQxLjUtOC41IDYyLjgtOC41czQyLjggMi45IDYyLjggOC41YzAgMCA0OC4xLTMzLjYgNjktMjcgMTMuNyAzNC43IDUuMiA2MS40IDIuNiA2Ny45IDE2IDE3LjcgMjUuOCAzMS41IDI1LjggNTguOSAwIDk2LjUtNTguOSAxMDQuMi0xMTQuOCAxMTAuNSA5LjIgNy45IDE3IDIyLjkgMTcgNDYuNCAwIDMzLjctLjMgNzUuNC0uMyA4My42IDAgNi41IDQuNiAxNC40IDE3LjMgMTIuMUM0MjguMiA0NTcuOCA0OTYgMzYyLjkgNDk2IDI1MiA0OTYgMTEzLjMgMzgzLjUgOCAyNDQuOCA4ek05Ny4yIDM1Mi45Yy0xLjMgMS0xIDMuMy43IDUuMiAxLjYgMS42IDMuOSAyLjMgNS4yIDEgMS4zLTEgMS0zLjMtLjctNS4yLTEuNi0xLjYtMy45LTIuMy01LjItMXptLTEwLjgtOC4xYy0uNyAxLjMuMyAyLjkgMi4zIDMuOSAxLjYgMSAzLjYuNyA0LjMtLjcuNy0xLjMtLjMtMi45LTIuMy0zLjktMi0uNi0zLjYtLjMtNC4zLjd6bTMyLjQgMzUuNmMtMS42IDEuMy0xIDQuMyAxLjMgNi4yIDIuMyAyLjMgNS4yIDIuNiA2LjUgMSAxLjMtMS4zLjctNC4zLTEuMy02LjItMi4yLTIuMy01LjItMi42LTYuNS0xem0tMTEuNC0xNC43Yy0xLjYgMS0xLjYgMy42IDAgNS45IDEuNiAyLjMgNC4zIDMuMyA1LjYgMi4zIDEuNi0xLjMgMS42LTMuOSAwLTYuMi0xLjQtMi4zLTQtMy4zLTUuNi0yeiIgLz48L3N2Zz4=)](https://github.com/SaberMage/spt-releases "Git repository")

![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMzA0IDQ4YzAtMjYuNS0yMS41LTQ4LTQ4LTQ4cy00OCAyMS41LTQ4IDQ4czIxLjUgNDggNDggNDhzNDgtMjEuNSA0OC00OHptMCA0MTZjMC0yNi41LTIxLjUtNDgtNDgtNDhzLTQ4IDIxLjUtNDggNDhzMjEuNSA0OCA0OCA0OHM0OC0yMS41IDQ4LTQ4ek00OCAzMDRjMjYuNSAwIDQ4LTIxLjUgNDgtNDhzLTIxLjUtNDgtNDgtNDhzLTQ4IDIxLjUtNDggNDhzMjEuNSA0OCA0OCA0OHptNDY0LTQ4YzAtMjYuNS0yMS41LTQ4LTQ4LTQ4cy00OCAyMS41LTQ4IDQ4czIxLjUgNDggNDggNDhzNDgtMjEuNSA0OC00OHpNMTQyLjkgNDM3YzE4LjctMTguNyAxOC43LTQ5LjEgMC02Ny45cy00OS4xLTE4LjctNjcuOSAwcy0xOC43IDQ5LjEgMCA2Ny45czQ5LjEgMTguNyA2Ny45IDB6bTAtMjk0LjJjMTguNy0xOC43IDE4LjctNDkuMSAwLTY3LjlTOTMuNyA1Ni4yIDc1IDc1cy0xOC43IDQ5LjEgMCA2Ny45czQ5LjEgMTguNyA2Ny45IDB6TTM2OS4xIDQzN2MxOC43IDE4LjcgNDkuMSAxOC43IDY3LjkgMHMxOC43LTQ5LjEgMC02Ny45cy00OS4xLTE4LjctNjcuOSAwcy0xOC43IDQ5LjEgMCA2Ny45eiIgLz48L3N2Zz4=)

# [The `spt api` surface](#the-spt-api-surface)

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

Three rules apply to `api` calls:

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

    spt api [--adapter <name[:profile]>] [--manifest <path>] <command> …

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

## [The two startup flows](#the-two-startup-flows)

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

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

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

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

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

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

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

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

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

## [Session lifecycle](#session-lifecycle)

### [`api seed --pid <pid> --session-id <id>`](#api-seed---pid-pid---session-id-id)

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

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

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

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

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

**`--session-id <sid>` — binding when the seed is gone.** A session that goes
live late, or after a daemon restart, finds no live seed; with `--session-id` the listener binds directly from the given harness session id (loud `SID_BIND:<id>` marker). The fallback fires **only** on `NO_SEED` — every
other refusal keeps its own diagnostic — and carries the same identity/auth
gates as a seeded bind (live-conflict refusal, dead-anchor refusal on the
parent pid). Without a live seed **and** without `--session-id`, `listen` refuses with `NO_SEED`.
> **Provenance caveat — same gates, weaker provenance.** A seed is a
> consume-once capability minted by the harness for exactly one anchor pid; `--session-id` is a **bearer string** — any local caller who knows a live
> session id can present it and revive that perch. Treat session ids as
> secrets: never log or publish them. (The local surface already trusts local
> callers — `--parent-pid` is an override — so this is a contract
> qualification for adapter authors, not a sandbox.)

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

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

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

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

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

### [`api boundary <clear|compact> <id> --to-session-id <new-sid> --session-id <prior-sid>`](#api-boundary-clearcompact-id---to-session-id-new-sid---session-id-prior-sid)

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

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

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

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

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

Pull the agent’s **resume context** to stdout, for a SessionStart hook to
inject as the session’s additional context after a `/clear`, `/compact`, or
fresh resume. Emits the durable two-tier mind — the agent’s role, its
cross-project live context, and the current project’s context — **plus** any
commune/signoff drop that has been written but **not yet synthesized** into
that durable context (as `<pending-commune>` / `<pending-signoff>` slices), so
a just-written delta is never invisible on resume. The project is resolved
from the perch’s recorded cwd. Read-only — it never writes the mind store.
Prints `NO-CONTEXT:<id>` on stderr (exit 0) when nothing is stored yet, the
adapter’s fresh-init signal.
> The *read-back-in* half of the commune/signoff file-drops (the *write* side): the agent drops its delta, spt-core synthesizes it into the durable
> mind, and `psyche-download` is how the next session reads that mind back in.
> Wire it into your SessionStart hook alongside `seed`/`listen`, and inject
> its stdout — that is how a resumed session keeps its accumulated context.

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

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

### [`api shutdown <id>`](#api-shutdown-id)

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

## [Activity and presence](#activity-and-presence)

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

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

### [`api echo-gate <set|clear> <id>`](#api-echo-gate-setclear-id)

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

### [`api presence <id>`](#api-presence-id)

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

### [`api driven-by <id>`](#api-driven-by-id)

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

## [Messages](#messages)

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

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

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

### [`api history-log <id>`](#api-history-log-id)

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

## [Workers](#workers)

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

### [`api worker-start <parent> [--agent-id <id>] [--agent-type <type>]`](#api-worker-start-parent---agent-id-id---agent-type-type)

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

Output channels follow the api status-line discipline:

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

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

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

### [`api worker-stop <id> --session-id <sid>` · `api worker-poll <id> --session-id <sid>`](#api-worker-stop-id---session-id-sid--api-worker-poll-id---session-id-sid)

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

## [Shells](#shells)

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

### [`api bind-shell --link <token>`](#api-bind-shell---link-token)

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

### [`api emit <id> <payload> --type <type> --link <token>`](#api-emit-id-payload---type-type---link-token)

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

### [`api owner-shutdown <id> --link <token>`](#api-owner-shutdown-id---link-token)

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

## [Introspection](#introspection)

### [`api capability`](#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](#conventions)

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