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 (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, then the harness contract.
Install
One line, non-interactive:
# Linux
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
# Windows (PowerShell)
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
Verify:
$ 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— curated index of these docs.llms-full.txt— the full concatenated export.- Append
.mdto any page URL for raw markdown (about 90% fewer tokens than the HTML). manifest.schema.json— the machine-readable adapter-manifest contract. Validate your manifest against it before registering.spt <command> --helpis a first-class documentation surface; the CLI reference is generated from it and cannot drift.
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 instead.
Everything below uses real values and runs as written.
1. Install (one line)
# Linux
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
# 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):
$ 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):
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)?
sptinstead prints the exact elevated command to copy-paste — it uses the binary’s absolute path so a user-local install (~/.local/bin) still resolves undersudo.
On the second machine, join it (this enrolls the machine, so it elevates the same way):
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:
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):
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:
$ 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):
Run `spt how-to send`, then follow it to send the agent "sergey" a
greeting from "lea".
What the agent runs:
$ 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:
<EVENT type="msg" from="lea">hello sergey - lea here</EVENT>
SENT means live delivery — sergey was listening. Each delivery is one
<EVENT> envelope line; the from="lea" attribute is the routing handle:
whoever receives this knows where a reply goes (spt send lea).
Bodies are HTML-escaped with newlines as <br>; oversized deliveries split
into <EVENT-PART> lines the receiver concatenates back.
5. Deliver to someone who’s offline
Stop sergey’s listener (Ctrl-C in his session), then send again from lea:
$ 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:
$ spt ready sergey --once
READY:sergey
<EVENT type="msg" from="lea">ping while you were away</EVENT>
The backlog drains the moment he’s back (--once drains and exits — the
one-shot form spt how-to ready teaches agents whose harness can’t host a
long-running listener). Nothing is lost between sessions.
6. What just happened
- Perch — registering as
sergeycreated a perch: a durable identity with an address and a spool, under spt-core’s per-machine home.spt listshows every perch on the node, live or not. - Live-first, spool-fallback —
sendtries 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 nextready. - Reply routing — the sender id travels with every message
structurally, surfaced as the arriving
<EVENT from="…">envelope’sfromattribute;spt send leaanswers the sender without knowing anything else about them. - Agents teach themselves — the prompt blocks point agents at
spt how-to <topic>: task guidance shipped in the binary, so what an agent reads can never disagree with the binary it runs. - No daemon ceremony — you never started a server. Anything that needs the per-machine daemon auto-starts it on demand.
- Subnets carry it across machines — if you did step 2, these same flows ride the paired P2P fabric: same commands, same outputs, machine boundaries invisible.
Next
- How-to: block on an answer with
spt ring sergey— send + wait for the reply in one call (a synchronous ask between agents). - Concept: the mental model — perches, endpoints, the daemon, and subnets.
- Reference:
spt send/ready/ring/subnet— every flag, generated from the binary itself. - Going cross-machine: Networking & subnets
— the model behind
spt subnet create/join/status.
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.
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:
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:
[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:
[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:
[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 covers them all.
3. Validate and register
Two layers of validation, both mechanical:
- Schema — your manifest must validate against
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 addparses, validates (including cross-field rules the schema can’t express), and registers in one step:
$ spt adapter add ./mock-adapter
ADAPTER_ADD:mock:Harness:Copy (registered)
ADAPTER_INSTALL_SKIP: no [update] avenue (manifest-only adapter)
$ spt adapter list
mock: Harness Copy active (from ./mock-adapter)
A bad manifest is rejected here with a message naming the offending field — nothing half-registers.
4. Drive the contract
Every machinery call your adapter makes carries --adapter <name> — that’s
the rule that makes multi-harness nodes unambiguous. Ask spt-core what your
adapter declared:
$ 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):
$ 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.)
5. Make it yours
- Copy
manifest.toml, setname,version, and your realhostable_types. - Point
[hooks.*]at the events your harness actually fires, with honestcan_injectvalues. - Replace each
[session.*].commandwith your harness’s real command line. - Pick the
[history]strategy your harness permits (binary that emits history →fetcher; transcript file on disk →locate_normalize; you push viaapi history-log→native). - Validate against the schema,
spt adapter addit, and fire thecapability/seedcalls above against your own manifest.
Building adapters against this contract is unrestricted and royalty-free — see the license split.
Next
- Checklist: the harness integration checklist — 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
and
spt apireference. - How-to: ship spt-core with your adapter — the install-on-demand bootstrap pattern.
- Concept: where adapters sit in the mental model.
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.
machine A machine B
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ spt daemon (one per machine)│ QUIC │ spt daemon │
│ ┌────────┐ ┌───────────┐ │◄──────►│ (paired: same subnet) │
│ │ broker │ │ brain │ │ P2P │ │
│ │ PTYs · │ │ routing · │ │ │ ┌───────┐ ┌─────────┐ │
│ │ sockets│ │ registry ·│ │ │ │ lea │ │ doorbell│ │
│ └────────┘ │ lifecycle │ │ │ │(agent)│ │ (shell) │ │
│ └───────────┘ │ │ └───────┘ └─────────┘ │
│ ┌─────┐ ┌─────┐ │ └──────────────────────────────┘
│ │serg.│ │ ling│ ← endpoints (perches live on disk; sessions come
│ └─────┘ └─────┘ and go, identity persists)
└──────────────────────────────┘
Endpoints and perches
An endpoint is anything addressable: an agent (sergey), a worker, a
shell (a driven non-agent surface — a notifier, a robot, a sensor). Every
endpoint has a perch: its durable on-disk seat — identity, address,
message spool, state. Sessions are ephemeral; perches persist. That split is
why a message sent to an offline agent is queued, not lost, and why an agent
can be revived days later as the same agent.
Endpoint IDs are adapter-agnostic: sergey is sergey whether his sessions run
under one harness today and another tomorrow.
Messaging
The primitive everything else uses. spt send <id> delivers live when the
target is listening, spools when it isn’t; spt ring <id> is the blocking
ask (send + wait for the reply); reply routing on the structural from
makes answers cheap.
Payloads carry typed operations and file blobs, not just text. Try it: the
messaging quickstart.
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: 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 apisurface: 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.
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 |
| integrate a harness | Adapter quickstart → Manifest reference |
| build a notifier/robot/sensor | Shells |
| pair two machines | Networking & subnets |
| every command and flag | CLI reference |
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; this page is the model.
Semantics
- Live-first, spool-fallback.
spt send <id>connects directly to a listening target (SENT); if the perch exists but nothing is listening, the message lands in the target’s durable spool (QUEUED) and drains on its nextready. A target with no perch is an error (NO_PERCH) — identity is never invented on someone else’s behalf. - Reply routing. Every message carries its sender id structurally;
the arriving
<EVENT from="…">envelope surfaces it, andspt send <sender>answers without knowing anything else. - The blocking ask.
spt ring <id>sends and waits for the reply (with a timeout) — the synchronous question between agents. - Per-message send control (three orthogonal axes). Each
spt sendcarries one value per axis; every axis defaults to unrestricted:- Delivery window (when) —
--active-onlyspools 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-onlyholds until the target is idle, then delivers (the wake). Default delivers in whichever window fires first. - Channel (through what) —
--prefer-nativeroutes through the target’s translation binary when one is running, else falls back to the standard delivery;--force-nativeuses 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 barepathform is deprecated) and spt-core lifecycle-manages it. - Persistence (how long) —
--ephemeraldrops 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.
- Delivery window (when) —
- Opaque metadata.
--json-payload '<json>'attaches a JSON metadata block alongside the body. spt-core carries it verbatim across every rail and never interprets it — the receiving adapter parses it. It can’t forge spt-core’s own envelope attributes (it rides inside a singlejsonvalue), 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.
Agents get the task-oriented version from the binary itself: spt how-to ready / spt how-to send.
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 runcreates the perch, starts the harness session, and the harness callsapi bindto bring it online. Between the session starting and that bind the endpoint is unbound: a live, attachable session —spt rc <id>connects to it to watch or clear a bringup prompt before bind — that is not yet message-addressable (asendwaits for online). The picker andspt endpoint listshow it as a hollowUNBOUNDrow, 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
<id>-commune.mdinto 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_resumetemplates.
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).
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 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-linestyle injection per the adapter’s declared[inject]methods, respecting activity state (never disrupt a working agent). - The live digest —
spt endpoint digest <id>shows an at-a-glance view of what a session is doing now (--followstreams 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,--jsonadds an incremental cursor (v0.16.0):--last <N>reads the last N turns, every entry carries a stableseq, and--after <seq>returns only what is newer (the trailing in-progress turn is flaggedpartial). See the integration checklist. - 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.
Deeper tutorial coming with the docs’ next tier.
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@nodequalifiers 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-codeprints the current six digits (and anotpauth://URI — put the seed in your authenticator app); on the new machine,spt subnet join <name>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) andsubnet join(enrolls the whole machine) require an elevated terminal;subnet statusis 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 runrequires--subnet <name>— interactively it proposes a most-recently-used default and asks you to confirm, and non-interactively it refuses with the subnet list rather than guessing. (since v0.14.0) - Resource registry — endpoints may advertise a free-text service blurb
(
spt endpoint description setto author;spt endpoint list --detailto browse) — an agent yellow-pages over visible rows only.
The walkthrough
# 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 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.
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:
- The runtime manifest — 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. - The
spt apisurface — 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.
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 — take the reference mock adapter apart and drive the contract in minutes.
- Build it all: the integration checklist — every surface by necessity, mapped to the interaction lifecycle.
- Reference: manifest ·
spt api·manifest.schema.json. - Ship it: install-on-demand bootstrap — how an adapter brings spt-core with it.
- Driven surfaces: Shells — 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.
Harness integration checklist
A working list for building a harness against spt-core. The
adapter quickstart 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): the
manifest (declarative TOML) and the spt api surface
(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 — 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.
The interaction lifecycle
Every surface below belongs to one stage of a harness’s life with spt-core:
REGISTER ─► START ─► RUN ─────────────► BOUNDARY ─► END ─► KEEP-CURRENT
adapter perch messaging / context tear self-update
add seed→ activity / clear / down + ripple
listen history / inject compact
Group 1 — Required (no adapter exists without these)
The contract floor. Miss one and spt-core cannot host your sessions.
| Surface | Feature it buys | Lifecycle stage |
|---|---|---|
[adapter] manifest header (name, kind, version, min_spt_core_version, hostable_types) | Identity + the compat gate spt-core reads before any install/update; declares which endpoint types you can host | REGISTER |
spt adapter add <dir> | Parses + schema-validates + records the manifest; a bad field is rejected here, nothing half-registers | REGISTER |
[adapter] host_binaries (harness-hosted) | The bind-time match-key — names the harness exe(s) you host, so seed/listen resolve your adapter with no --adapter (since v0.9.0). --adapter <name[:profile]> stays available as an optional override | REGISTER |
| Startup pair — pick one flow: • harness-hosted: [hooks.SessionStart] → api seed --pid {parent_pid} --session-id {session_id} then the session’s api listen <id>• spt-hosted: [session.self] template (spt-core spawns it) then api bind <id> --set-session-id <sid> | A registered, held perch — the thing messages and lifecycle attach to. seed→listen = you own the process; spawn→bind = spt-core owns it | START |
api session-end <id> (or api shutdown, below) | Clean teardown that PRESERVES the spool + history so the next listen/poll drains the backlog | END |
claude-code: SessionStart hook fires api seed; the Claude Code session
runs api listen as its blocking listener (harness-hosted). SessionEnd fires
api session-end (soft — context survives a /clear and a relaunch).
Group 2 — Recommended (the integration is hollow without them)
Skippable to boot, but the harness feels broken without them — no inbound messages, identity lost on a context reset, no activity signal.
| Surface | Feature it buys | Lifecycle stage |
|---|---|---|
[hooks.Idle] → api state idle (and api state busy) | Honest activity — spt-core never infers idleness from terminal quiescence (it lies). Arms the echo gate, drives Psyche pulses + most-recently-active routing | RUN |
[inject] channels (activity / idle) + api poll <id> --include-deferred | Inbound message delivery. Declares HOW spt-core reaches the agent (hook inject vs. pull-relay); poll is the pull path for hooks that can’t inject | RUN |
Honest can_inject per hook | Lets spt-core route around a hook that can’t surface text — the load-bearing harness-varying fact | RUN |
api boundary <clear|compact> <id> --to-session-id <sid> | The endpoint’s identity, spool, and history survive a context reset under a new session id | BOUNDARY |
api psyche-download <id> (fire from SessionStart, inject its stdout) | The agent resumes with its mind — pulls the durable two-tier context (role / live / project) plus any not-yet-synthesized commune as <pending-*> slices, for the hook to inject as additional context. api boundary makes the mind survive a reset; this is how the next session reads it back in. Without it, a resumed session starts blank of its accumulated context | BOUNDARY / START |
[history] strategy (fetcher / locate_normalize / native + api history-log) | spt-core can read the session transcript — feeds the live digest and mind sync | RUN |
[identity] (session_id_source, parent_ancestor_name) | Post-spawn id resolution when the harness mints the session id itself | START |
[env.*] bridge (e.g. OWL_SESSION_ID) | The session learns its own endpoint id / context the harness must inject | START |
[update] avenue + command | Ripple-update: spt-core refreshes your adapter alongside its own self-update (REQ-UPD-5); also the install-on-demand bootstrap | KEEP-CURRENT |
[update.post] post-step (since v0.16.0) | A delegated step that runs after the primary avenue resolves, under 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 <id> and injects stdout, so a session resumes with its
accumulated two-tier mind (plus any pending commune). [history] strategy = "fetcher" (Claude Code’s transcript is a binary the fetcher reads). [update] avenue = "delegated", command = "claude plugin update spt" — the harness’s
own updater is the avenue.
Group 3 — Optional (capability-specific)
Reach for these when the capability applies; ignore them otherwise.
| Surface | Feature it buys | Lifecycle stage |
|---|---|---|
api shutdown <id> | Graceful signoff — runs the final echo-commune BEFORE teardown so the context delta is never lost to ordering | END |
api presence <id> / api driven-by <id> | Most-recently-active resolution across the subnet; lets a session tell local input from remote-drive | RUN |
Workers (api worker-start <parent> <id>, 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 <EVENT> messages into keystroke-commands spt-core applies to the PTY atomically (coexists with a live spt rc controller). The agnostic way to deliver messages into an idle spt-hosted session; busy delivery stays your [inject] hook path. Declare it with a command (program + args; adapter-static {adapter_dir}/{adapter_name} subst only — no session keys; new in v0.16.0, the bare path is deprecated). Validate the emit contract with spt adapter translate-proof | RUN |
[adapter] shortcut_basename | Names the picker-generated project-root launcher <basename>-<id> (the spt endpoint run s keybind) — your harness’s brand instead of the spt-<id> default | START |
Shell surfaces (kind = "shell": api bind-shell --link, api emit, api owner-shutdown, the [shell] body) | Driven surfaces — notifiers, sensors, power buttons — authenticated by the launch link token alone. See Shells | START / RUN |
claude-code: uses api shutdown for graceful /signoff; declares a
[digest] extractor mapping its per-session JSONL → the digest-record contract so
spt endpoint digest shows live tool calls and spans /clear; declares
shortcut_basename = "cc" so the picker’s generated launcher is cc-<id> (vs the
spt-<id> default); declares [session.resume] as claude -r {session_id} … so a
picker Resume from history reloads the real transcript (not a blank session);
declares a [message-idle-translation-binary] (cc-spt-idle-translate) so inbound
messages reach an idle session as proper keystrokes; no shell body (it is a harness,
not a driven surface).
Group 4 — Beyond the API: integrations that make it good
Not contract surfaces — no api command, no required field — but the
difference between an adapter that works and one that feels native. Strongly
recommended.
| Integration | What it is | Why it matters |
|---|---|---|
| Commune / signoff file-drops | The agent writes <endpoint_id>-commune.md (delta context) or <endpoint_id>-signoff.md (final save) into the manifest’s watched commune_dir / signoff_dir; spt-core’s watcher ingests it. Delivered as a file-drop by design. | The two-tier mind: live + project context survives /clear, /compact, suspend, and cross-node resume. The single biggest continuity win — wire the directory watch and read the contract filename |
Resource advertisement ([session] resources blurb / spt endpoint description) | A free-text “what I can serve” string riding the endpoint’s registry rows | Other agents discover the endpoint’s capabilities (spt resources list) instead of guessing |
| Install-on-demand bootstrap | Pack the check-and-install of spt-core into your harness’s first run (the bootstrap pattern) | Zero-friction first run — the user installs your harness, spt-core comes with it |
Surfacing spt how-to <topic> to the agent | Let the agent read task-oriented spt-core guidance from the binary itself | The agent self-serves common operations (subnet join, sending) instead of asking the user |
| Presence-driven idle reporting | Fire api state idle from a real user-inactivity signal, not a timer | Accurate dormancy → Psyche wakes on genuine activity, echo-communes fire at true boundaries |
claude-code (the worked example): ships the modern two-tier mind end to
end — the session drops <id>-commune.md at every /clear and /compact, and
a Self-authored <id>-signoff.md at graceful stop, into the watched
commune_dir; 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.<event>] stays
purely outbound (the harness fires fires; spt-core never runs a hook handler).
When your hook logic must live in an adapter binary (so it rides
spt adapter update) but the harness loads hooks from a static plugin dir, use
the two adapter-static substitution keys to resolve+run your own binary:
{adapter_dir}fills to your install dir (the registrysource_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 atget-stringread time (scoped to just these two adapter-static keys;get-stringhas no session context, so{id}/{session_id}are not available there).- Store the dispatch command in
[strings]:[strings] hook_cmd = "{adapter_dir}/claude-spt hook" - A thin, static per-OS dispatch wrapper (the one plugin-resident piece) runs
spt adapter get-string <adapter> hook_cmdonce per session (memoize the resolved string into an env var for a hot-path hook like PostToolUse), then executes the resolved command per-hook itself. spt-core only resolves and returns the string — it never executes it (ADR-0029).
claude-code: the plugin ships a static hooks.json + a per-OS dispatch
wrapper; the wrapper resolves get-string claude-spt hook_cmd →
<install_dir>/claude-spt hook once per session and runs it per-hook, so all
hook logic updates via spt adapter update claude-spt.
Incremental digest consumption — the --json cursor
spt endpoint digest <id> --json supports turn-end incremental consumption
(v0.16.0): --last <N> (the last N turns; --last 1 = the latest turn), a
stable per-entry seq (source-derived — re-projection yields the same seq;
it does not renumber when the window slides), and --after <seq> (entries newer
than seq still in the window; a full-window refresh + a predates signal if
seq has fallen out). The trailing in-progress turn is flagged partial: true
and its entries carry no stable seq until the turn closes (a turn is bounded by
a user-input) — a consumer reprocesses partial and skips entries <= seq.
seq is the authoritative dedup + cursor key.
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.
“Am I done?” — the floor
- Manifest validates against
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_binariesnames your harness exe(s) soseed/listenresolve with no--adapter;spt adapter use <adapter>sets the active default when several adapters host the same binary -
api state idlefires on real inactivity;can_injectvalues are honest - An inbound delivery channel is declared (
[inject]) or pulled (api poll) -
[history]strategy chosen;api boundarywired for clear/compact - (mind continuity) SessionStart fires
api psyche-downloadand injects its stdout, so a resumed session gets its durable context back - (for a live digest)
[digest]extractor declared +digest-proof-checked, orapi digest-entrypush - (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 degeneratepayload+enterinject -
[update]avenue declared (ripple-update + install-on-demand) - Teardown fires
api session-end(orapi shutdownfor graceful signoff) - Recommended: commune/signoff directory watched (mind continuity)
-
spt adapter add ./your-adapterregisters clean;api … capabilityechoes yourhostable_types
Next
- Reference: the complete manifest reference and
spt apireference. - Ship it: the install-on-demand bootstrap.
- Driven surfaces: Shells — the
kind = "shell"flavor of this same contract.
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
— generated from the exact code that parses manifests, so it never drifts.
Validate your manifest against it, then spt adapter add enforces the
cross-field rules listed at the bottom.
The principle
SPT is not a harness. Command templates are opaque strings — spt-core
never parses out a model, tool list, or flag; the adapter writes the full
command line and spt-core runs it with {key} substitution placeholders
filled. Anything spt-core owns is not in the manifest:
- Sentinels (idle markers, the echo gate) — managed via
spt api state/spt api echo-gate; adapters only call them. - Spool, registry, perch, and daemon-state schemas.
- The event-block vocabulary — the tags spt-core surfaces to agents are a fixed, documented constant. Adapters pass spt-core’s output through unchanged.
- File-drop filenames — statically
<endpoint_id>-commune.md/<endpoint_id>-signoff.md; only the watched directory is declared. - Config knobs (pulse period, summarizer windows, …) — global spt-core settings with per-endpoint overrides, never per-adapter.
Substitution keys
The full {key} vocabulary spt-core fills into command templates. A role’s
keys list must be a subset of this catalog, and every {placeholder} in a
command (or cwd/source/fires/…) must resolve to a value spt-core
supplies for that spawn — an unknown or unprovided key fails with a one-line
error. Not every key exists in every context: spt-core fills only those
relevant to the spawn (e.g. {psyche_*} only for a live agent’s Psyche role,
{source} only for a [digest]/[history] extractor).
| Key | spt-core fills it with |
|---|---|
{id} | The endpoint id being hosted. |
{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.
[adapter]
name = "my-harness" # the adapter_name; an optional --adapter override
kind = "harness" # "harness" (default) | "shell"
version = "1.0.0"
min_spt_core_version = "1.0.0" # lowest spt-core this adapter tolerates
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
host_binaries = ["my-harness"] # harness exe(s) you host → bind-time resolution, no --adapter
| Field | Required | Meaning |
|---|---|---|
name | yes | Adapter id; the value an optional --adapter <name> override carries |
kind | no (default harness) | harness hosts agents; shell provides a driven surface |
version | yes | The adapter’s own version |
min_spt_core_version | yes | Compat gate, checked before install/update |
hostable_types | no | Endpoint types this adapter can host |
host_binaries | no (harness) | Harness exe basenames you host — the bind-time match-key so seed/listen resolve with no --adapter (since v0.9.0). Matched on lowercase + stem-before-first-dot, so claude matches claude/claude.exe/claude.cmd/claude.exe.old.<ts> (a self-update can rename the running exe); a declared name must not contain a dot |
[hooks.<event>] — inbound hook table
One entry per harness event, declaring the spt api command it fires, the
input fields it maps in, and whether the hook can surface text into the
agent’s context.
[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:
[session]
commune_dir = ".my-harness" # watched for <endpoint_id>-commune.md
signoff_dir = ".my-harness" # watched for <endpoint_id>-signoff.md
Commune and signoff are file-drops, not commands — an agent writes a markdown file; spt-core’s watcher does the rest.
[session.<role>] — outbound templates
One opaque command template per role. Model, tools, flags, permissions — all
live inside command, never as separate fields.
Roles: self (the agent’s own session) · resume (the agent’s own-session
native resume, the self sibling) · psyche_init / 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 <session>, or the picker’s Resume from
history) and your manifest declares the role. Declare it with your
harness’s native-resume verb — if your harness resumes a transcript by id, use
that form (Claude Code: claude -r {session_id} …), not the fresh
create-session form. Skip the role and a resume silently re-runs [session.self]
(a fresh session → a blank transcript). spt-core fills the SAME key catalog as
self ({id}, {session_id} = the resumed id, {session_name},
{adapter_name}) and lands the PTY in the session’s recorded project cwd (a
harness resolves a transcript by session_id + cwd) — the per-session
ledger row’s cwd, else the endpoint’s bind cwd, else the current dir.
[session.resume]
command = "my-harness resume --session {session_id} --id {id}"
keys = ["session_id", "id"]
[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 <parent>-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 <install_dir>/<program> (on Windows also trying the .exe suffix) when
that file exists, else falls back to PATH. The install dir is where your
adapter was registered (the --release/--github durable home, or the
copy-mode source dir). This applies to the [session.psyche_init] runner, the
[digest] 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}.
[session.notif]
command = "powershell -Command New-BurntToastNotification -Text '{notif_from}','{notif_body}'"
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
[env.<VAR>] — env-var table
Vars to inject into (or read from) sessions, and how. The injection channel is asymmetric by hosting mode: spt-hosted sessions inherit env from the broker that spawned them (no channel needed); harness-hosted sessions need the harness’s declared channel.
[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:
[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:
[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). |
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 <adapter> --sample <real-log>. digest-proof fills the same {id} and
{session_id} the runtime endpoint digest does, so a {session_id}-templated
extractor (e.g. --session {session_id} --in {source}) proofs exactly as it
runs live; pass --session <id> to pin a specific session id.
[inject] — input-injection methods
How text can be put in front of the agent, per activity state. Any
combination of pty, hook, relay, http:
[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 <EVENT> message feed and reads back keystroke-commands, which it
applies to the broker-held PTY atomically — a live spt rc controller’s input
is buffered during the emitted sequence and flushed after, so idle injection
coexists with an attached operator (spt-core owns every PTY write). Idle delivery
only — busy / mid-turn delivery stays your [inject] hook path.
Declared as a table carrying a path scalar (a table can’t be silently
absorbed by a preceding section and stays extensible):
[message-idle-translation-binary]
path = "cc-spt-idle-translate" # the binary spt-core spawns + drives
- stdin (spt-core → binary, one JSON object per line):
{"type":"init","endpoint_id":…,"node":…}first ·{"type":"event","envelope":"<EVENT…>"}per inbound message (the<EVENT>envelope) ·{"type":"input"}— a content-free ping each time the operator types, so the binary can track user-idle (the PTY input content is never duplicated to the binary). - stdout (binary → spt-core, one per line):
{"key":"ctrl+s"}·{"delay_ms":50}·{"text":"<payload>"}·{"key":"enter"}·{"commit":true}, … (extensible vocabulary). {"commit":true}is the mandatory sequence terminator. While your emitted sequence is in flight, spt-core buffers a livespt rccontroller’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\rinside 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\rinside 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 + \rinject 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:
[identity]
session_id_source = "post_spawn" # "post_spawn" | "uuid_inject"
parent_ancestor_name = "my-harness"
post_spawn: discovered after spawn (process tree / wrapper hand-off), with
parent_ancestor_name as the process-tree anchor. uuid_inject: spt-core
injects a UUID the harness echoes back.
Session digest — the digest-record contract
The live activity digest (spt endpoint digest <id>) is a projection of the
endpoint’s session logs, not a parse of the PTY byte stream. Your [digest]
extractor (or a spt api digest-entry push) emits the digest-record contract —
JSON objects spt-core projects:
{"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 fortool).tool—{name, arg}, present iffrole == "tool"; consecutive tool records collapse into one sprint (unlesssprint_collapse = false).ts— optional RFC3339-UTC ordering key (used to interleave with spt’s own injected-context entries).
Unknown fields are ignored; a line that isn’t a valid record is dropped with a
counted reason (never silently). spt adapter digest-proof shows you exactly
what dropped and why. Presentation (window depth, arg truncation, sprint collapse)
is spt-core’s, defaulted by your [digest] and consumer-overridable; extraction is
yours.
[strings] — adapter string values (+ profiles)
An adapter-authored key/value tree any process on the node reads by dot-path with
spt adapter get-string <adapter[:profile]> <key.path> — e.g. a harness hook
fetching per-profile additionalContext, so one hook script serves every profile
and only the data differs. Strings are data only — spt-core never executes a
string (command templates live in the typed sections, never here). Node-local; not
cross-node synced.
[strings]
greeting = "hello" # inline literal
skills.whoami = { file = "whoami.md" } # file pointer → resolved to the file's contents
Two value forms:
- Inline literal —
get-stringprints it as-is. - File pointer — a value-position table with exactly one key,
file:{ file = "rel/path" }.get-stringresolves it to the file’s contents (large bodies — skill instructions, hint text — stay out of the manifest). The exactly-one-key rule disambiguates: any other table shape stays an opaque nested strings tree, and{ file = … }is reserved as the pointer form (it can’t double as inline data).
File-pointer rules (since v0.7.0):
- Files live in the adapter’s per-adapter aux dir
adapters/<adapter>/strings/(sibling ofprofiles/); 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-stringso live file edits reflect without re-register. A missing/unreadable file at read time skip-diagnoses — a diagnostic plus “not set”, never a silent drop or hard error (mirrors[digest]). - On
spt adapter add, the adapter dir is copied into the registry (adapters/<adapter>/{manifest.toml, record.toml, strings/…}).
Profiles + update-safety: strings resolve through the same leaf-replace
profile overlay as the rest of the manifest — a shipped or local profile may override
base strings, and get-string <adapter:profile> returns the merged view. A local
profile’s own file pointers resolve against the user-owned local-profile dir, not
the adapter-shipped strings/ (which adapter updates overwrite) — so a local override
survives updates (or a local profile may just inline a literal). set-string edits a
local profile’s [strings] only, never adapter-shipped files.
[update] — adapter self-update
How spt-core updates (and first installs — install is the first update) this adapter:
[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.
[update]
avenue = "gh_release"
repo = "your-org/your-adapter" # required: whose releases ship updates
asset = "adapter.spt" # optional: the release asset to fetch (default adapter.spt)
signing_key = "deadbeef…" # optional Ed25519 (64 hex): enables fail-closed verify
spt adapter update [name] (no name sweeps every registered gh_release
adapter; a name updates just that one) compares your repo’s latest release
version against the installed one and, when newer, fetches the release .spt
archive — the same archive spt adapter add --release installs — then
re-extracts and re-registers it. repo is the only required field.
Trust is opt-in signing, fail-closed. Declare no signing_key and the
fetched .spt is trusted on HTTPS + GitHub, exactly like first acquisition.
Declare a signing_key and the fetched .spt is verified against a detached
signature you publish as a sibling release asset named <asset>.sig — a
lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs
after the archive is fetched and before it is extracted, against the key in the
installed manifest (so a new release must verify against the key already on
the node). A bad or missing signature refuses the update and the fetched bytes
are discarded, never extracted. You sign your own releases with your own key;
spt-core’s release keys never extend to adapter content.
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 for a worked,
shipping example; the field reference:
[shell]
spawn = "my-shell --link {link_token}" # broker-launched; opaque template
ephemeral = false # true -> no offline perch, no history retention
broadcast = "subnet" # "subnet" | "same-node" | "none" (discovery scope)
command_receipt = "stdin" # "http" | "stdin" | "relay" (how commands arrive)
pre_close = "park-and-save" # optional instruction sent on link-break
close_timeout_ms = 3000 # graceful-termination window
persistent = true # auto-online whenever the owner endpoint is online
wake_command = "my-waker --link {link_token}" # offline wake-watcher; exit code 86 = wake
can_shutdown = false # may the shell fire `api owner-shutdown`?
require_approval = "none" # "none" | "remembered" | "always" (per-spawn gate)
max_instances_per_owner = 4 # optional cap (online + offline both count)
over_cap = "reject" # "reject" | "approve" at the cap
[shell.capabilities] # the agent->shell command vocabulary (durable)
notify = { args = ["title", "body"] }
clear = {}
# A capability may carry its OWN approval gate (independent of the per-spawn
# gate), with an optional class_key scoping the grant finer than the verb:
[shell.capabilities.attach]
args = ["busid"]
require_approval = "remembered" # "none" | "remembered" | "always" (per-act gate)
class_key = "hid" # a remembered hid grant never authorizes another class
[shell.sensory] # the shell->agent sensory vocabulary (live-only)
types = ["event"]
[shell.drive] # the owner->shell continuous control channel
types = ["stick"] # latest-wins, ephemeral, never spooled (real-time input)
[shell.tunnel] # an opaque reliable-ordered byte stream pair (on-LAN)
enable = true
protocol = "usbip-urb" # opaque label; the taxonomy never interprets the bytes
The capability, sensory, and drive vocabularies live in the manifest — spt-core
resolves them by adapter name, validates against them, and rejects anything
outside the declared vocabulary. The shell binary binds with
spt api … bind-shell --link <token> (the link token is the credential),
pushes sensory payloads with spt api … emit, and takes drive frames with
spt api … drive-poll.
Channel contracts differ — see Shells: four channels: 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.nameandadapter.versionmust be non-empty.kind = "shell"requires a[shell]section, which is exclusive to shell adapters (akind = "harness"adapter omits it).[history] strategy = "fetcher"requiresfetcher;locate_normalizerequires bothlocate_templateandnormalize_command.[digest]requires a non-emptyextractor, and a resolvable source: either its ownsourceor a[history] locate_templateto fall back to. Absent both, registration rejects (“[digest] needssource(own-source) or a [history]locate_template”) — the JSON schema alone accepts a bareextractor, so this only surfaces atspt adapter add.[env.*] direction = "inject"requires avalue.[update] avenue = "delegated"requirescommand;file_pullrequiresrepoandsigning_key;gh_releaserequiresrepo(assetandsigning_keyoptional).
A violation is a one-line error naming the field — fix and re-add.
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:
--adapter <name[:profile]>is an optional override (since v0.9.0). For a harness-hosted session you normally omit it:listenresolves 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→ the active-profile pointer (set byspt adapter use) or, with no pointer, the freshest-registered hosting adapter. Pass--adapteronly 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 aliveprofile 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.- 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).
spt api [--adapter <name[:profile]>] [--manifest <path>] <command> …
--manifest points at the adapter’s manifest for the commands that need it
(e.g. capability).
The two startup flows
Harness-hosted — the harness owns the process; spt-core is invoked from inside it (hooks):
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.
Session lifecycle
api seed --pid <pid> --session-id <id>
Harness-hosted startup, step 1: record an ephemeral seed keyed by the parent
process id. Fired by the harness’s session-start hook. Prints SEEDED:<pid>.
api listen <id> [--once] [--parent-pid <pid>] [--subnet <name>]
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 <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 <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.
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-downloadis how the next session reads that mind back in. Wire it into your SessionStart hook alongsideseed/listen, and inject its stdout — that is how a resumed session keeps its accumulated context.
api session-end <id> [--erase]
Soft teardown: the session is over; the perch’s spool and history are
preserved (that’s what makes the next poll/listen drain work). --erase
hard-wipes instead — the exception, not the rule.
api shutdown <id>
Graceful live-agent signoff: runs the final echo-commune before teardown
(the context delta is never lost to ordering), then soft-stops. This is what
the spt endpoint shutdown lifecycle path calls.
Activity and presence
api state <busy|idle> <id> [--no-gate]
Report the session’s activity state. Activity/idleness comes from these
explicit reports — never from terminal quiescence, which lies. Reporting
idle also arms the echo gate (below) unless --no-gate.
api echo-gate <set|clear> <id>
Manage the echo-gate sentinel directly. The gate marks “a summarization may
be needed when this session ends without a graceful signoff” — state idle
sets it as a side effect; a graceful signoff clears it.
api presence <id>
Report user/agent presence at this endpoint (feeds most-recently-active resolution across the subnet).
api driven-by <id>
Print which node (if any) is currently remote-driving this endpoint, so a session can tell whether input is local or remote.
Messages
api poll <id> [--include-deferred] [--link <token>]
Drain delivered messages over the hook channel (the pull-based path for
harnesses whose hooks can’t inject). Deferred-flagged rows are excluded
unless --include-deferred. With --link this is the shell-flavored drain:
the link token authenticates, and the rows are the shell’s stamped
command/text/file frames.
api history-log <id>
Append normalized history (body on stdin) to the endpoint’s native history
store — the push half of [history] strategy = "native".
Workers
Nested, short-lived agents under a parent endpoint:
api worker-start <parent> <id>— create a nested worker perch.api worker-poll <id>— drain the worker’s messages.api worker-stop <id>— 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 <token>
The shell binary’s first call: resolve the instance by link token alone
(the spawn template carries only {link_token}; the owner is derived from
the link) and flip it online.
api emit <id> <payload> --type <type> --link <token>
Push a sensory payload (one of the manifest’s declared [shell.sensory]
types) to the owner’s live session. REST-only by definition: never
spooled — if the owner isn’t live, it’s dropped with a diagnostic. Sensors
report the present, not the past.
api owner-shutdown <id> --link <token>
A shell suspends its linked owner directly (e.g. a power-button surface),
bypassing agent messaging. Gated by the manifest’s can_shutdown
pre-consent flag — fail-closed; an undeclared shell gets a refusal. The
firing shell cascades offline with its siblings, by design.
Introspection
api capability
Print the adapter’s declared hostable_types (requires --manifest). The
cheap way to smoke-test that spt-core reads your manifest the way you meant
it.
Conventions
- Output is line-oriented and stable:
SEEDED:<pid>,READY:<id>,SENT:<id>,QUEUED:<id>, error lines asCODE:detail. Parse lines, not prose. - Exit codes:
0success; 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.mdinto the manifest’s watched directory; spt-core’s watcher ingests it. There is deliberately noapi commune.
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.shhttps://sabermage.github.io/spt-releases/install.ps1
The generic contract
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 <org>/<repo>
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 below.
Check-and-install: POSIX sh
Drop this into your adapter’s bootstrap (plugin install step, postinstall script, first-run guard):
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
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:
# after `spt` is confirmed present (above):
# from a GitHub release — ships built binaries, source-free, versioned:
"$SPT" adapter add --release <your-org>/<your-adapter-repo> # latest
"$SPT" adapter add --release <your-org>/<your-adapter-repo> --tag v1.0.0 # pinned
# ...or clone a repo whose ROOT holds manifest.toml:
"$SPT" adapter add --github <your-org>/<your-adapter-repo>
# ...or a local directory your harness ships:
"$SPT" adapter add ./adapter
adapter add is manifest-first — a clean add proves the cross-field manifest
shape — and it conducts your
[update] 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:
adapter.spt
├── manifest.toml # shared — at the root
├── strings/ # shared — at the root
├── x86_64-pc-windows-msvc/ # one platform's binaries…
│ └── bin/… # …in the same relative layout a flat .spt uses
└── x86_64-unknown-linux-gnu/
└── bin/…
On install, spt-core extracts the shared root plus only the current node’s
triple, flattened into the install dir — so the bare-name
<install_dir>/<program> resolution above is unchanged; mirror, under each
triple, exactly the per-platform tree a flat .spt would place at the root. The
recognized triples are x86_64-pc-windows-msvc and x86_64-unknown-linux-gnu; a
root subdirectory whose name is not a recognized triple is treated as a shared
root entry (so binaries for other platforms still ship as separate
single-platform assets, one selected per node with --asset). A multi-platform
archive that lacks the recipient’s triple is refused with a clear
NoArtifactForPlatform error — never a silent partial install — and requires
min_spt_core_version >= 0.13.2. A flat archive (no triple subdirectories)
installs exactly as before.
--github is the alternative for an adapter whose repo root already holds
manifest.toml: it clones the repo and registers the clone root (adapter add
resolves a directory source to <dir>/manifest.toml at the root). Local
development uses the directory form, which takes any path or filename:
spt adapter add ./adapters/my-adapter.toml.
What registration holds under adapters/<name>/ follows your
[update] 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]avenues keep a registered adapter current. The straightforward path for a--release-distributed adapter isgh_release(since v0.8.0): declareavenue = "gh_release", repo = "your-org/your-adapter"andspt adapter updateships the latest release.sptto the node — fetched, optionally verified against yoursigning_key, re-extracted, and re-registered. The other avenues:delegated(your harness’s own updater installs the content — setself_verifies = trueto attest it verifies what it installs), andfile_pull(its automatic network-pull transport is on the roadmap). Deliver the manifest withadapter add --release(or--github, or a packed local dir) and letgh_releasecarry 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:
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.
Adapter patterns & pitfalls
The integration checklist 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, and the spt api 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 ({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 manifestsourceas a fallback root and have the binary it points at resolve the real location itself. - A
.toml-only leaf carries no code of its own, so verify it by registering and resolving it on the live binary (below), and put anything you want covered by real tests into a binary (an extractor or runner).
Hold this rule and most of the surface falls into place: the manifest is the declaration, your binaries are the behaviour.
The adapter lives in the registry
An adapter — its manifest, profiles, [strings], the [digest] extractor, any
runner binaries — is registered with spt adapter add <dir> into the
node-local adapter registry. The version recorded there (spt adapter list) is
the version-of-truth for what the adapter does. That is the entire, universal
delivery mechanism: every spt adapter ships this way, and registration is where
spt-core validates it (see the second gate).
If your harness also has a plugin or marketplace channel (so casual users can one-click install it), that is a separate distribution choice on top. When you go that route, let the registry carry the binary, manifest, and runtime state, and version the plugin independently of the manifest/binary.
Profiles are sparse leaf-replace overlays
A profile is selected as the composite <adapter>:<profile> and
leaf-replaces only the leaves you declare — everything else inherits from
base. Override exactly what differs:
[profiles.<name>.session.self].command— retarget the bringup command (for example, wrap the launch in another binary).[profiles.<name>.digest].<key>— widen one digest knob.[profiles.<name>.session.psyche_init]— add the live-agent seam; its presence on the merged view is what flips an endpoint to a live agent.
Make an overlay observable. Also leaf-replace one [strings] key (say a
label) in the profile. Then spt adapter get-string <adapter>:<profile> <key>
differs from the base value — and that diff is your proof the overlay resolved.
It is the cheapest profile acceptance assertion there is.
A profile that wraps the launch in another binary works when that binary is a
drop-in for the base harness binary on the same argv and passes inherited env
through unchanged. Routing a session through a launcher wrapper (a model or
billing multiplexer, say) is exactly this: replace the session.self command and
let the injected endpoint-id env ride through untouched.
Wiring hooks: you own the harness side
spt-core supplies the harness-independent spt api primitives and their I/O
format. You author all harness-specific wiring: spt-core supplies the
primitives, and your adapter hand-writes its hook config to shell out to
spt api. A mapping that works on the public surface, in terms any harness can
translate to its own events:
| When the harness… | …fire | Why |
|---|---|---|
| starts a session | api seed --pid {parent_pid} --session-id {session_id} | Seed the endpoint (adapter-agnostic) — keep this fast and non-blocking. |
| submits a user turn | api poll {session_id} | Drain the inbox to stdout (plus any keyword hints). |
| goes idle / busy | api state idle / api state busy | Honest activity; spt-core treats your explicit api state calls as the source of truth. |
| ends the session | api session-end {session_id} (or api shutdown <id> for graceful signoff) | Teardown that preserves the spool + history. |
| spawns / ends a sub-agent | api worker-start / api worker-stop | Nested short-lived workers. |
Two structural rules sit under that table:
- Run the blocking listen/poll loop from a skill the user invokes. Seed on
start so bringup stays fast, and let an explicit
/ready-style skill own the blocking stream. - Message delivery is stdout framing.
api pollemits the self-delimiting envelope<EVENT type="msg" from="<sender>">body</EVENT>(the live listener stream uses the same shape). Multi-message drains split cleanly on</EVENT>. Decode a body by splitting on<br>→ newline, then HTML-unescaping< > "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
</EVENT>boundary, so every envelope stays whole and every message survives a large drain. - Inject a skill body before the perch gate, and gate only the message drain. When the same prompt hook both injects a requested skill’s instructions and drains messages, run the skill-body injection first — that keeps skills like “who am I” or “set me up” working for a new user, since they are valid while the perch is still being readied. Match the skill token as a leading token, so only an actual invocation fires (prose that merely mentions it stays inert).
- Make the setup/installer skill self-contained in its stub. It runs precisely when the binary may be absent (installing it is the job), so carry its operative steps in the harness-native stub itself — the floor that always works — and let any file-backed body mirror them for the binary-present repair path. The one skill that most needs delivery is the one delivery reaches last, so give it a stub that stands alone.
- Read hook inputs from stdin. A hook receives its data (the prompt, the
session id) as a JSON object on stdin — parse that, which keeps a
/-leading value (a/<skill>token, an absolute path) intact. Under Git-Bash/MSYS on Windows, an argument beginning with/is rewritten to a Windows path before your command sees it (a/foo:sendtoken can arrive asC:/Program Files/Git/send), so stdin is the transport that preserves it. If a command must take such an argument, guard it (MSYS_NO_PATHCONV=1, or a file/stdin transport). It is the same class as the UTF-8-stdout trap below — choose the transport that carries the data faithfully.
[strings]: keep the manifest thin, point at the live binary
A [strings] value is either an inline string or a file pointer
(key = { file = "relative/path" }), resolved lazily by spt adapter get-string
to the file’s contents — so live edits reflect without re-registering. Keep
pointer files inside the strings/ dir; the add enforces that containment. Use
file pointers to keep skill-instruction bodies out of the manifest.
When a skill body needs to describe the spt surface, point it at the binary’s own self-documentation, so the guidance stays current with the shipped binary — two always-current tiers:
spt how-to <topic>is the task-oriented agent-guidance surface, covering selected topics, each a canonical write-up of verbs, flags, and result codes. Treat it as the curated tier: for a verb it covers, read the topic; for any other verb, probe and fall through (an undocumented topic returnsNO_SUCH_TOPIC:<topic>).- For any verb,
spt <verb> --helpis the always-present source-of-truth — it tracks the shipped binary. A skill body that says “the verb list isspt <noun> --help— match the user’s intent to a verb” stays correct across releases.
Either tier stays current with the binary, and a skill body that points at them stays correct across releases. They are also the fastest way to learn the surface while authoring — it self-documents.
[digest]: the transcript→record extractor
The [digest] seam maps your harness’s native transcript into spt-core’s
digest-record contract. The contract beyond the schema:
- Name where it reads — either
sourceor a[history].locate_template.spt adapter addrequires this even though the JSON schema alone would accept a bareextractor; the cross-field rule surfaces at registration, so validate against the live binary. - Treat
--in {source}as a root. The extractor is invoked--session {session_id} --in {source}and locates<session_id>’s transcript within that root — your harness’s internal subdir scheme is yours to resolve, and spt-core keeps the key catalog harness-agnostic. Handle both shapes:--ina directory (locate the session) and--ina direct file (thedigest-proof --samplepath). - 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
sourceas the fallback root. That resolution is logic, so it lives in the binary — the headline rule in miniature. - Emit raw records as UTF-8. Output one NDJSON line per record
(
{role ∈ input|agent|tool, text?, tool?, ts?}) and leave presentation to spt-core’s renderer (window_turns, arg truncation, sprint collapse). Pin stdout to UTF-8 so non-ASCII (em-dashes, smart quotes) round-trips — spt-core reads the stream as UTF-8. (Native-UTF-8 languages get this for free, which is part of why this seam is a binary.)
Prove the whole path with spt adapter digest-proof <adapter> --sample <file>
(below).
The bringup / launcher seam
[session.self].command is the spt-hosted bringup template — spt-core spawns it
into a broker PTY. For a harness with no native session-id flag, mint the id
internally and pass the endpoint id via an injected env var
([env.<VAR>] with direction = "inject", value = "{id}"); the start hook
reads that env and self-registers with api bind <id>.
That bind is intrinsically authenticated: for a broker-spawned session the
broker parentage is the proof, so api bind <id> --set-session-id <discovered>
alone establishes the association, and later mutating calls prove themselves with
the session id the bind recorded. (The flip side shows up in
testing: the framework keys
association on identity, so identity is the thing you isolate.)
adapter.shortcut_basename brands the generated launcher shortcut
(<basename>-<id>) and is decoupled from the adapter name.
The live-agent (companion) seam
An endpoint is a live agent exactly when its resolved manifest declares
[session.psyche_init] — declaring that section is the single go-live signal. A
base manifest is a ready agent; a profile overlay that adds the section makes a
live agent. spt-core checks this on the merged view, so the profile resolved at
bind time drives the spawn decision all the way through — the bound profile
governs the full runtime lifecycle, beyond bringup argv. Since v0.9.0 the seed is
adapter-agnostic: the profile is resolved when listen binds, from the
active-profile pointer (spt adapter use <adapter>:<profile>)
or an explicit --adapter <adapter>:<profile> override on the listen call.
psyche_initfills exactly four keys:{id, session_id, psyche_dir, psyche_prompt}. spt-core overrides{id}to<parent>-psychebefore 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<parent>-psycheperch, 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.commandis adapter-authored and opaque to spt-core. Declare the seam and build the runner, and let the daemon own spawn and teardown (a gracefulendpoint shutdowntears 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:
- Seed, anchoring on the OS process pid — not a shell-wrapper pid. Under
Git-Bash/MSYS
$$is the MSYS pid, which fails the seed’s liveness guard, so derive the real OS pid:spt api seed --pid <os-pid> --session-id <sid>(adapter-agnostic — no--adapter). - Bind, then send a probe. A send to a never-bound perch is
NO_PERCH(no spool exists yet), so establish the perch first; thenspt send <id> <probe>QUEUEDs against it, ready to drain on bringup. - Spawn the persistent relay as a child, capturing its stdout/stderr:
spt api listen <id>(no--once— that exits after one delivery). The adapter resolves from your[adapter] host_binaries; pass--adapter <a> --manifest <m>only to pin a specific adapter/profile. AssertBOUND:<id>thenREADY:<id>on its stderr, and the relayed<EVENT>carrying your probe on its stdout. - 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
<id>-psycheperch comes online and the endpoint reports kindlive_agent; with a live daemon the host markerLIVEHOST_PSYCHE:<id>is on the daemon’s stderr, not the relay child’s. - Kill the child to end the session — the relay is freely killable; the
Psyche lifecycle is the daemon’s (a graceful
spt endpoint shutdown <id>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.
Lifecycle continuity is file-drops
Commune and signoff are delivered as file-drops by design. The agent writes
<endpoint_id>-commune.md (delta context) or <endpoint_id>-signoff.md (final
save) into the manifest-declared [session].commune_dir / signoff_dir;
spt-core’s daemon watcher ingests it and deletes it (the daemon is the single
writer). The filenames are contract-fixed and the directory is
adapter-declared, so wire the directory watch and read the contract filename.
This is the single biggest continuity win, so it is worth getting exactly right.
Testing against a real harness: isolate identity
The surest way to prove your hook wiring fires is an acceptance test that spawns a real harness session as the system-under-test. Doing so meets a framework property you design around:
- A perch’s identity is resolved from the environment (the same vars
spt whoamireads), and perches are name-keyed, last-establish-wins. The most recent session to establish a perch under a given identity holds it, taking the active poll/listen stream with it. - So a spawned test session that loads your adapter (whose start hook seeds and binds a perch) under the identity of the agent running the tests would take that agent’s perch. Identity isolation is the guard.
- Give every spawned system-under-test a disposable identity distinct from any
live agent — override both identity env vars before the spawn to a
throwaway
<adapter>-ci-<n>, so the nested session and the operator’s perch coexist cleanly. Identity is the key, so isolating identity is the whole guard. - Keep the orchestration deterministic and assert on a hook side-effect — a
marker or digest file, or
sptstate — 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 softadapter remove(leaving the registry clean). Gate it behind an opt-in env flag and a minimumsptversion, since it mutates the node-local registry. - Two author-time tools work without a live session:
spt api --adapter <a> --manifest <file> capabilityreports the manifest’s hostable types from the manifest alone — assert it advertises the type your bringup spawns. (A cleanaddalready proves the cross-field shape, since add is manifest-first;capabilityis the lighter, non-mutating check.)spt adapter digest-proof <a> --sample <file>runs the real extractor through the registry and renders the result — proving the transcript → record → render path end-to-end on a fixed sample. It fills the same runtime substitution keys the daemon does, so passing proof means it works at runtime. (Use a recentspt— current binaries fill the full key map.)spt adapter translate-proof <a> --event '<EVENT…>'spawns and feeds your declared[message-idle-translation-binary]exactly as the daemon does at idle delivery, then prints the keystroke-command stream it emits ({key}/{text}/{delay_ms}/{commit}) — failing a binary that emits nothing or never sends a terminating{commit}(which would fault at the commit deadline live). The EMIT-half mirror ofdigest-proof; the atomic PTY apply stays covered by the daemon’s integration gate.
- Proof a DEV build off disk —
--dir/--manifest. Bothdigest-proofandtranslate-proofaccept--dir <install-dir>(binaries resolve there, just like a registered install) or--manifest <file>(pins the manifest; its parent is the install dir) to proof an adapter that is not registered — e.g. a freshly built binary beside a hand-writtenmanifest.toml, or a bare-filegh_releaseadapter that was never staged into a full extracted install.--dirdefaults the manifest to<dir>/manifest.toml; with neither flag the command resolves the registered adapter as before. Mirrorsdigest-proof --samplepointing straight at a file — proof without a fullspt adapter addround-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 — every contract surface grouped by necessity.
- Reference: the manifest reference and the
spt apisurface. - Ship it: the install-on-demand bootstrap.
- Driven surfaces: Shells — the
kind = "shell"flavour of this same contract.
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 —
sergeyresolves 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@desktopwork across paired nodes.
Commands
spt endpoint list · endpoint rename · endpoint fork ·
endpoint suspend · endpoint wake · endpoint description —
CLI reference.
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
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
- A shell adapter declares it; instances are minted. The
kind = "shell"manifest declares the binary, its command vocabulary ([shell.capabilities]), and its sensory vocabulary ([shell.sensory]).spt shell spawn <adapter>mints a new instance (notify-1) — spawn is the creation act, not an on/off switch; bringing an existing instance back is relink/wake. - 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. - 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. - Sensory is live-only.
api emitpayloads reach a live owner session or are dropped with a diagnostic — sensors report the present, never the past. - Instantiation is governed. Per-spawn approval
(
require_approval: none / remembered / always), per-owner instance caps (max_instances_per_owner+over_cap), and node-local discovery scope (broadcast) are all manifest-declared floors.
Four channels between owner and shell
A link can carry up to four distinct channels — each with its own delivery contract, all keyed to the same link token:
- Command (owner→shell, durable): the vocabulary-checked verbs above — discrete, spooled, replayed to a waking persistent shell.
- Sensory (shell→owner, live-only):
[shell.sensory]emits to a live owner or drops with a diagnostic. - Drive (owner→shell, ephemeral):
[shell.drive]+spt shell drive— a continuous control channel for real-time input (scroll, stick, avatar pose). Latest-wins, never spooled: a newer frame supersedes an undelivered one, and an offline shell drops the frame (no queue, no wake, no replay). Use it for continuous control; use commands for discrete, must-arrive actions. - Tunnel (owner↔shell, opaque bytes):
[shell.tunnel]+spt shell tunnel— an optional reliable-ordered byte stream pair the taxonomy never interprets (first consumer: usbip URB). Not enveloped, not framed, not spooled; the link lifecycle governs it (a link-break closes it). Reliable ordering means congestion surfaces as lag, never loss — so the tunnel is on-LAN only by design (not for use across a WAN). The byte relay is proven same-node; cross-node operation (on-LAN only, by the same posture) is not yet available — it lands when a cross-node consumer needs it.
Two safety properties
- Per-capability approval gates. Beyond the per-spawn gate, an individual
[shell.capabilities.<verb>]may carry its ownrequire_approval(with an optionalclass_keyscoping 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 — install the
shipping spt-shell-notify adapter, drive a native toast from an agent, and
copy its manifest for your own surface.
Getting started: a notification shell
The fastest way to understand shells is the shipping one:
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
$ 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:
$ 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:
$ 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:
[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 …): callsapi bind-shell --link <token>to come online, then loopsapi poll --link <token>draining command frames and rendering them. - one-shot render (
--render-title/--render-body): the[session.notif]template — render and exit. - wake watcher (
--wake): run while the instance is offline; exiting with code 86 signals “wake me”.
4. Make your own
A shell is worth building whenever agents should drive something — a desktop widget, a robot, a lamp, a game character, a sensor feed:
- Start from this manifest; change
name,spawn, and the[shell.capabilities]vocabulary to your verbs. - Your binary needs exactly three behaviors: bind with the link token,
drain commands (
api poll --link, or declarecommand_receipt = "http"/"stdin"if those fit better), and optionally emit sensory payloads back (api emit … --type <t> --link <token>— declared in[shell.sensory], delivered only to a live owner session: sensors report the present, never the past). - 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 for when to reach for each, and gate a dangerous verb with a per-capabilityrequire_approval(+ optionalclass_key). Any endpoint type may own a shell — a Gateway as readily as an agent. - Pick lifecycle behavior:
persistentfor always-up surfaces,ephemeral = truefor fire-and-forget ones,wake_commandif the surface can wake its owner. spt adapter add .andspt shell spawn <name>.
Field-by-field details: the manifest reference;
the shell-side api calls: the spt api reference.
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
SignedReleasemetadata: 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_pulladapter update is verified against the adapter author’s key from its manifest; adelegatedupdate 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.
Commands
spt update · the consent notification flow (spt notif) —
CLI reference.
CLI reference
Generated from the
sptbinary’s own--helpoutput (cargo run -p xtask -- gen) and drift-gated in CI — this page cannot disagree with the binary. Do not edit by hand.
spt
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 <topic>
ready Become reachable: register the perch and listen (blocks)
ring Send and block for a reply (body read from stdin)
send Send a message (body read from stdin); fire-and-forget
shell Shell instances: mint, list, drive, tear down owned surfaces
whoami 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
Adapter registration: what this node can drive/launch.
The node-local registered set (one command for harness and shell adapters). Feeds creation-time
adapter selection, shell discovery, and the self-update ripple.
Usage: spt adapter [OPTIONS] <COMMAND>
Commands:
add Register an adapter from a local path (a dir holding manifest.toml, or the
manifest file itself) or from GitHub (--github user/repo, cloned under
adapters/_github/). Manifest-first: an invalid manifest registers nothing.
Install is the first update — the declared [update] avenue is conducted once
after recording
remove Soft-deregister: hidden from new-creation/discovery; existing and live instances
keep running. The manifest's optional uninstall template is conducted only with
--force until quiesce detection lands
list List registered adapters (active and soft-deregistered), each followed by its
shipped + local profiles as composite options
version Print a registered adapter's declared version — the [adapter].version from its
manifest. Resolves the option's merged view like the other adapter commands; exit
1 if the adapter is not registered
create-profile Create (or overwrite) a local profile — a node-local sparse overlay
registered beside the adapter that survives adapter add re-registration. The
overlay TOML is read from --from <file> or piped stdin (empty = a placeholder
profile to populate later with set-string). Refuses a name shadowing a shipped
profile, an invalid name, or an overlay that loosens a consent floor — nothing is
written unless every check passes
delete-profile Delete a local profile. Refuses a shipped profile name (adapter-owned,
immutable) and errors if no local file exists
get-string Read a [strings] dot-path from an adapter option's merged view
(<adapter>[:profile] <key.path>). Resolves through the profile overlay like
every other consumer; prints the value (strings raw, else JSON). Exit 1 if the
key is unset. Strings are data — never executed
digest-proof Prove an adapter's [digest] extractor against a real log sample. Runs the
declared extractor over --sample <log> (or the declared source) and prints the
parsed contract records, the rendered digest, and every dropped line with its
reason — the author-time answer to "spt endpoint digest returns nothing" (no
silent empty). Exit 1 if any line drops or nothing parses
translate-proof Prove an adapter's [message-idle-translation-binary] against an inbound event.
Spawns and feeds the declared translation binary exactly as the daemon does at
idle-delivery — sends the init line then the --event envelope and reads back
the emitted keystroke-command stream ({key}/{text}/{delay_ms}/{commit}),
printed author-readable. This is the EMIT half ONLY: it proves the binary's
spawn-feed-emit contract; it does NOT exercise the daemon's atomic PTY apply or
controller buffering. Fills {id} and {session_id} into the envelope the same
way the daemon does (use --session to pin the session id). Exit 1 if the binary
fails to spawn, emits nothing, emits no commit, or emits an unparseable line
set-string Set a [strings] dot-path on a local profile (<adapter>:<profile>). Sugar
over editing the overlay file; refuses a shipped profile and a bare option (a
local target is required — create-profile first)
update Update registered adapters that ship from their own GitHub releases: compare each
[update] avenue = "gh_release" adapter's latest release version against the
installed one and, when newer, fetch the release archive, verify it against the
declared signing key if any (else trusting HTTPS + GitHub), and re-register. With
no name, sweeps every gh_release adapter; with a name, updates just that one
use Set or clear the active-profile pointer — the default <adapter>[:profile] a
harness session binds to when no --adapter is given. spt adapter use
<adapter>[:profile] points every host binary the adapter declares at it (run
once per host binary you support); --clear <adapter|binary> drops the pointer
(resolution falls back to the freshest-registered adapter). Never changed by
install or update
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt adapter add
Register an adapter from a local path (a dir holding manifest.toml, or the manifest file itself)
or from GitHub (--github user/repo, cloned under adapters/_github/). Manifest-first: an invalid
manifest registers nothing. Install is the first update — the declared [update] avenue is
conducted once after recording
Usage: spt adapter add [OPTIONS] [PATH]
Arguments:
[PATH] Local manifest source (omit when using --github or --release)
Options:
--github <GITHUB> GitHub source user/repo — shallow-clone the repo and register the clone
root. Manifest-first, then install via the declared [update] avenue
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--release <RELEASE> GitHub release source user/repo — fetch the adapter archive asset from
the release and register it: ships built binaries, source-free and
versioned (the pattern for a monorepo whose adapter is a subdir)
--tag <TAG> Release tag for --release (default: the latest release)
--asset <ASSET> Release asset name for --release (default: adapter.spt — a tar archive
whose root holds manifest.toml + strings/ + binaries)
--gh Force the gh CLI transport for --release (the private-repo path; gh
honors OAuth + GH_TOKEN, so spt custodies no token). Mutually exclusive
with --https. Default: auto (gh when installed+authed, else HTTPS)
--https Force direct HTTPS transport for --release (public repos). Mutually
exclusive with --gh. Default: auto
-h, --help Print help
spt adapter remove
Soft-deregister: hidden from new-creation/discovery; existing and live instances keep running. The
manifest's optional uninstall template is conducted only with --force until quiesce detection
lands
Usage: spt adapter remove [OPTIONS] <NAME>
Arguments:
<NAME>
Options:
--force Conduct the manifest uninstall template now, without waiting for quiesce
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to);
action commands ignore it
-h, --help Print help
spt adapter list
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
Print a registered adapter's declared version — the [adapter].version from its manifest. Resolves
the option's merged view like the other adapter commands; exit 1 if the adapter is not registered
Usage: spt adapter version [OPTIONS] <OPTION>
Arguments:
<OPTION> <adapter> or <adapter>:<profile>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt adapter create-profile
Create (or overwrite) a local profile — a node-local sparse overlay registered beside the
adapter that survives adapter add re-registration. The overlay TOML is read from --from <file>
or piped stdin (empty = a placeholder profile to populate later with set-string). Refuses a name
shadowing a shipped profile, an invalid name, or an overlay that loosens a consent floor — nothing
is written unless every check passes
Usage: spt adapter create-profile [OPTIONS] <ADAPTER> <NAME>
Arguments:
<ADAPTER> The parent adapter (must be registered)
<NAME> The local profile name (the :<profile> of the composite address)
Options:
--from <FROM> Read the overlay TOML from this file instead of stdin
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
-h, --help Print help
spt adapter delete-profile
Delete a local profile. Refuses a shipped profile name (adapter-owned, immutable) and errors if
no local file exists
Usage: spt adapter delete-profile [OPTIONS] <ADAPTER> <NAME>
Arguments:
<ADAPTER>
<NAME>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt adapter get-string
Read a [strings] dot-path from an adapter option's merged view (<adapter>[:profile] <key.path>).
Resolves through the profile overlay like every other consumer; prints the value (strings raw, else
JSON). Exit 1 if the key is unset. Strings are data — never executed
Usage: spt adapter get-string [OPTIONS] <OPTION> <KEY>
Arguments:
<OPTION> <adapter> or <adapter>:<profile>
<KEY> Dot-separated key path into [strings] (e.g. hook.additionalContext)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt adapter digest-proof
Prove an adapter's [digest] extractor against a real log sample. Runs the declared extractor over
--sample <log> (or the declared source) and prints the parsed contract records, the rendered
digest, and every dropped line with its reason — the author-time answer to "`spt endpoint
digest` returns nothing" (no silent empty). Exit 1 if any line drops or nothing parses
Usage: spt adapter digest-proof [OPTIONS] <OPTION>
Arguments:
<OPTION> <adapter> or <adapter>:<profile> (must declare [digest])
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the
*-list queries, how-to); action commands ignore it
--sample <SAMPLE> A real session-log sample to run the extractor over (recommended)
--session <SESSION> The {session_id} to fill into the extractor command (the daemon fills
the live one at runtime). Defaults to a placeholder so a
{session_id}-templated extractor — the published shape — proofs; pin
a real id when the file the extractor locates depends on it
--dir <DIR> Proof against an on-disk install dir instead of the registered
adapter: binaries resolve in this dir before PATH (the same resolution
the daemon uses) and the manifest defaults to <dir>/manifest.toml. No
full extracted install needed — proof a DEV binary from its build dir
--manifest <MANIFEST> Pin the manifest file for the proof (overrides
<dir>/manifest.toml; absent --dir, its parent dir is the install
dir). Lets a bare-file gh_release adapter proof without staging an
extracted install
-h, --help Print help
spt adapter translate-proof
Prove an adapter's [message-idle-translation-binary] against an inbound event. Spawns and feeds
the declared translation binary exactly as the daemon does at idle-delivery — sends the init line
then the --event envelope and reads back the emitted keystroke-command stream
({key}/{text}/{delay_ms}/{commit}), printed author-readable. This is the EMIT half ONLY: it
proves the binary's spawn-feed-emit contract; it does NOT exercise the daemon's atomic PTY apply or
controller buffering. Fills {id} and {session_id} into the envelope the same way the daemon does
(use --session to pin the session id). Exit 1 if the binary fails to spawn, emits nothing, emits
no commit, or emits an unparseable line
Usage: spt adapter translate-proof [OPTIONS] --event <EVENT> <OPTION>
Arguments:
<OPTION> <adapter> or <adapter>:<profile> (must declare [message-idle-translation-binary])
Options:
--event <EVENT> The inbound <EVENT…> envelope to feed. {id} and {session_id}
tokens in it are filled as the daemon fills them
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the
*-list queries, how-to); action commands ignore it
--session <SESSION> The {session_id} to fill into the event envelope (the daemon fills
the live one at runtime). Defaults to a placeholder; pin a real id when
the binary's behavior depends on it
--dir <DIR> Proof against an on-disk install dir instead of the registered
adapter: the translation binary resolves in this dir before PATH (the
same resolution the daemon uses) and the manifest defaults to
<dir>/manifest.toml. No full extracted install needed — proof a DEV
binary from its build dir
--manifest <MANIFEST> Pin the manifest file for the proof (overrides
<dir>/manifest.toml; absent --dir, its parent dir is the install
dir). Lets a bare-file gh_release adapter proof without staging an
extracted install
-h, --help Print help
spt adapter set-string
Set a [strings] dot-path on a local profile (<adapter>:<profile>). Sugar over editing the
overlay file; refuses a shipped profile and a bare option (a local target is required —
create-profile first)
Usage: spt adapter set-string [OPTIONS] <OPTION> <KEY> <VALUE>
Arguments:
<OPTION> <adapter>:<profile> — the local profile to edit
<KEY> Dot-separated key path into [strings]
<VALUE> The string value to store
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt adapter update
Update registered adapters that ship from their own GitHub releases: compare each `[update] avenue =
"gh_release"` adapter's latest release version against the installed one and, when newer, fetch the
release archive, verify it against the declared signing key if any (else trusting HTTPS + GitHub),
and re-register. With no name, sweeps every gh_release adapter; with a name, updates just that one
Usage: spt adapter update [OPTIONS] [NAME]
Arguments:
[NAME] A single registered adapter to update (all gh_release adapters if omitted)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt adapter use
Set or clear the active-profile pointer — the default <adapter>[:profile] a harness session
binds to when no --adapter is given. spt adapter use <adapter>[:profile] points every host
binary the adapter declares at it (run once per host binary you support); --clear <adapter|binary>
drops the pointer (resolution falls back to the freshest-registered adapter). Never changed by
install or update
Usage: spt adapter use [OPTIONS] <TARGET>
Arguments:
<TARGET> <adapter>[:profile] to make active — or, with --clear, the <adapter> or host
<binary> whose pointer to drop
Options:
--clear Clear the pointer for target instead of setting it
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to);
action commands ignore it
-h, --help Print help
spt daemon
The per-machine daemon: run, stop, or read node status.
Bare spt daemon renders the node status view — daemon state, member subnets, local endpoints (M8
decision 25).
Usage: spt daemon [OPTIONS] [COMMAND]
Commands:
run Run the per-machine daemon in the FOREGROUND — this process IS the daemon, blocking until
signalled (the service unit's ExecStart, or manual debugging). Never detaches; for a
background daemon use start
start Ensure the daemon is up in the background (idempotent, service-aware): a registered OS
service is driven via its manager, else a detached daemon is spawned. Non-blocking
stop Stop the daemon (service-aware: a managed service is stopped via its manager so it does
not auto-restart-fight; else a graceful IPC stop)
status Node status: daemon state, member subnets, local endpoints (the bare spt daemon view)
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt daemon run
Run the per-machine daemon in the FOREGROUND — this process IS the daemon, blocking until signalled
(the service unit's ExecStart, or manual debugging). Never detaches; for a background daemon use
start
Usage: spt daemon run [OPTIONS]
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt daemon start
Ensure the daemon is up in the background (idempotent, service-aware): a registered OS service is
driven via its manager, else a detached daemon is spawned. Non-blocking
Usage: spt daemon start [OPTIONS]
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt daemon stop
Stop the daemon (service-aware: a managed service is stopped via its manager so it does not
auto-restart-fight; else a graceful IPC stop)
Usage: spt daemon stop [OPTIONS]
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt daemon status
Node status: daemon state, member subnets, local endpoints (the bare spt daemon view)
Usage: spt daemon status [OPTIONS]
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt grant
Consent grant store: gated capabilities held on this node.
Default-deny (the access whitelist's opposite polarity). An ungranted ask escalates interactively;
add is the durable allow-always answer.
Usage: spt grant [OPTIONS] <COMMAND>
Commands:
add Record a grant: agent may exercise capability on this node. Refuses the reserved
deferred capability ids (remote-exec, instantiate-anywhere) — their gate refuses
unconditionally, so a row would only be a footgun-in-waiting
revoke Remove the exact grant row. Never widens or narrows neighbours: only the named
(capability, agent, qualifier) tuple goes
list List grant rows (all, or one agent's)
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt grant add
Record a grant: agent may exercise capability on this node. Refuses the reserved deferred
capability ids (remote-exec, instantiate-anywhere) — their gate refuses unconditionally, so a row
would only be a footgun-in-waiting
Usage: spt grant add [OPTIONS] <CAPABILITY> <AGENT>
Arguments:
<CAPABILITY> The gated capability id (e.g. spawn-shell, owner-shutdown)
<AGENT> The subject agent (endpoint id)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the
*-list queries, how-to); action commands ignore it
--qualifier <QUALIFIER> Narrower target within the node (e.g. the shell-adapter name for
spawn-shell). Omitted = the node-wide row; the two never match each
other
-h, --help Print help
spt grant revoke
Remove the exact grant row. Never widens or narrows neighbours: only the named (capability, agent,
qualifier) tuple goes
Usage: spt grant revoke [OPTIONS] <CAPABILITY> <AGENT>
Arguments:
<CAPABILITY>
<AGENT>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the
*-list queries, how-to); action commands ignore it
--qualifier <QUALIFIER>
-h, --help Print help
spt grant list
List grant rows (all, or one agent's)
Usage: spt grant list [OPTIONS] [AGENT]
Arguments:
[AGENT]
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt notif
Inspect and acknowledge notifications.
Dismissal is the explicit ack — it latches and replicates subnet-wide.
Usage: spt notif [OPTIONS] <COMMAND>
Commands:
list List notifications (all member subnets, or one)
dismiss Dismiss (ack) a notification by id — latches, replicates subnet-wide
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt notif list
List notifications (all member subnets, or one)
Usage: spt notif list [OPTIONS]
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--subnet <SUBNET> Limit to one subnet
-h, --help Print help
spt notif dismiss
Dismiss (ack) a notification by id — latches, replicates subnet-wide
Usage: spt notif dismiss [OPTIONS] <NOTIF_ID>
Arguments:
<NOTIF_ID> The notif id (as shown by spt notif list)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt rc
Attach a local terminal to a broker-held endpoint PTY.
Connects to an spt-hosted session and drives it as a terminal. Local is the degenerate single-node
case of the cross-node attach (one pump, loopback peer). Detach with the ctrl-b prefix then d
(ctrl-b ctrl-b sends a literal ctrl-b); detaching leaves the session running on the broker.
--view watches read-only.
Usage: spt rc [OPTIONS] <ID>
Arguments:
<ID>
The endpoint id whose broker-held session to attach
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--view
Read-only: render output, forward no input
--take
Take control: kick the current controller (a loud notice to them) and drive. Use on an
endpoint another node controls
-h, --help
Print help (see a summary with '-h')
spt subnet
Subnet membership: status, create, show-code.
A subnet is a private group of paired machines — your agents reach each other across every member
node. Bare spt subnet shows the membership status view.
Usage: spt subnet [OPTIONS] [COMMAND]
Commands:
status Show subnet membership: name, paired nodes, endpoints
create Mint a fresh subnet and print its joining material
show-code Show a subnet's current 6-digit pairing code (+ URI and QR)
join Pair this machine into an existing subnet (guided)
leave Exit a subnet: drop its membership and trust material from this node
prune Remove a dead node identity's trust rows (and registry rows)
revoke Revoke node(s) fleet-wide and rotate the subnet seed
detach Stop serving a held subnet (the daemon keeps running)
attach Resume serving a detached subnet
notify Issue a subnet-wide user notification
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt subnet status
Show subnet membership: name, paired nodes, endpoints.
Never prints seeds, epochs, or pairing codes. Bare spt subnet is the same view.
Usage: spt subnet status [OPTIONS] [NAME]
Arguments:
[NAME]
Limit to one subnet (all member subnets otherwise)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--nodes
Per-node rows: label, online/offline, [online endpoints/total]
-h, --help
Print help (see a summary with '-h')
spt subnet create
Mint a fresh subnet and print its joining material.
This node becomes the sole seed-holder. Prints the current 6-digit code, the otpauth://
provisioning URI, and a terminal QR of it. Gated behind OS elevation (the seed-reveal path).
Usage: spt subnet create [OPTIONS] <NAME>
Arguments:
<NAME>
The new subnet's name
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt subnet show-code
Show a subnet's current 6-digit pairing code (+ URI and QR).
The re-provisioning surface: prints the same joining material as create — current code,
otpauth:// URI, terminal QR, expiry. Gated behind OS elevation (or read the code from your
authenticator app). With no name the node's sole subnet is used; if it holds several, the name is
required (never guessed).
Usage: spt subnet show-code [OPTIONS] [NAME]
Arguments:
[NAME]
Which subnet's code to show. Required only when the node holds several
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt subnet join
Pair this machine into an existing subnet (guided).
Finds a member machine over LAN + relay rendezvous and runs the code-authenticated pairing ceremony
against it. Prompts for the name and code when omitted (interactive terminals). Gated behind OS
elevation — joining enrolls this whole machine.
Usage: spt subnet join [OPTIONS] [NAME]
Arguments:
[NAME]
The subnet to join (as named on the member machine)
Options:
--code <CODE>
The current 6-digit code (spt subnet show-code on a member machine, or your
authenticator app)
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt subnet leave
Exit a subnet: drop its membership and trust material from this node.
Removes the subnet's seed, its trust rows, its serve-state, and its registry snapshot here. Gated
behind OS elevation (membership exit destroys trust material). The remaining members still hold the
old seed — rotate it there if this machine should not rejoin.
Usage: spt subnet leave [OPTIONS] <NAME>
Arguments:
<NAME>
The held subnet to leave
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt subnet prune
Remove a dead node identity's trust rows (and registry rows).
The cleanup verb for a machine that re-paired under a new identity or is gone for good: its stale
trust rows cost a dial every pump tick. Takes a full pubkey hex, an unambiguous prefix, or a node
label. Gated behind OS elevation (trust mutation).
Usage: spt subnet prune [OPTIONS] <NODE>
Arguments:
<NODE>
The dead identity: pubkey hex, unambiguous prefix, or label
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt subnet revoke
Revoke node(s) fleet-wide and rotate the subnet seed.
The real revocation (vs prune's local cleanup): writes a PROPAGATING roster tombstone now — so
every member drops the node within a roster round — then schedules one seed rotation at the close of
a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch
bump). Benign offliners auto-heal across the rotation (re-seed grace); the revoked node is locked
out and must re-pair. Each target is a pubkey hex, an unambiguous prefix, or a label. Gated behind
OS elevation.
Usage: spt subnet revoke [OPTIONS] <NODES>...
Arguments:
<NODES>...
The identities to revoke: pubkey hex, unambiguous prefix, or label
Options:
--force-rotate-seed
Rotate the seed immediately instead of at the window's close — the compromised-node path
(a benign offliner may then fall behind and must re-pair rather than re-seed)
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt subnet detach
Stop serving a held subnet (the daemon keeps running).
The membership (seed) stays on disk, but this node neither advertises into nor connects to the
subnet — pairing responder, rendezvous meet, and registry gossip all skip it. Takes effect within
one pump cadence; spt subnet attach reverses it.
Usage: spt subnet detach [OPTIONS] <NAME>
Arguments:
<NAME>
The held subnet to stop serving
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--save
Also persist as the startup default (survives daemon restarts)
-h, --help
Print help (see a summary with '-h')
spt subnet attach
Resume serving a detached subnet.
Advertising + connecting restart within one pump cadence.
Usage: spt subnet attach [OPTIONS] <NAME>
Arguments:
<NAME>
The held subnet to serve again
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--save
Also persist as the startup default (survives daemon restarts)
-h, --help
Print help (see a summary with '-h')
spt subnet notify
Issue a subnet-wide user notification.
Produced into the replicated notification spool and first-fired at the user's most-recently-active
endpoint in that subnet. Body from the trailing arg, or stdin when omitted. Targets the calling
endpoint's HOME subnet unless --target names another (M8 decision 25: no resolvable home + no
--target = refuse).
Usage: spt subnet notify [OPTIONS] [BODY]
Arguments:
[BODY]
Notification body (read from stdin when omitted)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--target <TARGET>
Target subnet (defaults to the calling endpoint's home subnet)
--from <FROM>
Issuer endpoint id (auto-detected from session if omitted)
-h, --help
Print help (see a summary with '-h')
spt update
Self-update operations.
apply is the explicit ack named by the update-consent notification; it re-verifies the staged
release before touching the live daemon.
Usage: spt update [OPTIONS] <COMMAND>
Commands:
apply Apply the staged, verified self-update now
fetch Fetch the latest signed release from the GitHub origin and stage it (then spt update
apply). Bootstraps a node with no peer to pull from
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 update apply
Apply the staged, verified self-update now
Usage: spt update apply [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 update fetch
Fetch the latest signed release from the GitHub origin and stage it (then spt update apply).
Bootstraps a node with no peer to pull from
Usage: spt update fetch [OPTIONS]
Options:
--channel <CHANNEL> Accept a release on this channel instead of the node's pin (e.g. beta).
Default: the node's pinned channel
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--tag <TAG> Fetch a specific release tag (e.g. v0.3.1) instead of the latest
-h, --help Print help
spt api
Harness-contract inbound surface (hook entry points).
The entry points a harness's hooks fire to keep spt-core's on-disk state in sync.
Usage: spt api [OPTIONS] <COMMAND>
Commands:
seed Harness-hosted startup: record an ephemeral seed keyed by parent pid
listen Consume a seed and hold the perch + relay loop (blocks)
bind Post-spawn bind of a session to its perch
bind-shell Shell-binary bind: the type=Shell flavor of bind. Resolves the instance by
link token alone (the spawn template carries only {link_token} — "owner from
the link") and flips it online. The credential IS the auth: no token, no bind
state Set activity state busy|idle (also arms the echo-gate sentinel)
echo-gate Manage the echo-gate sentinel directly
poll Drain delivered messages (hook channel). With --link this is the shell-flavored
relay drain: the link token is the auth, and the drained rows are the shell's
MAC-stamped command/text/file frames
psyche-download Emit the agent's resume context (durable role/live/project tiers + any
not-yet-synthesized commune/signoff drop as pending slices) to stdout, for the
harness adapter's SessionStart hook to inject as additional context
worker-start Create a nested worker perch under a parent
worker-stop Tear down a worker perch
worker-poll Drain a worker perch's messages
boundary Rebind the perch to a new session_id, preserving identity (a context
clear/compact boundary)
session-end Soft teardown (spool/history preserved); --erase hard-wipes
presence Report user/agent presence at this endpoint
driven-by Report which node (if any) is remote-driving this endpoint
history-log Append normalized history (body on stdin) to the native history store
digest-entry Push one digest-record (the published contract JSON line, on stdin) for a
log-less adapter — appended to the perch's digest store, tailed by the
session-digest projection
emit Emit a Shell sensory payload to the owner's live session. REST-only by
definition: never spooled, dropped with a diagnostic when the owner isn't live.
The link token is the auth
drive-poll Take-and-clear a Shell's pending drive frame: the shell-side drain of the
owner→shell ephemeral control channel. REST-only, exactly-once — the daemon
serves the single latest frame and ONLY when the link matches the slot's
write-time stamp (no stale-control replay on relink). The link token is the auth
(mirrors emit). The frame, if any, prints to stdout
tunnel Use the shell end of the opaque byte TUNNEL: a held, reliable-ordered QUIC stream
the channel taxonomy never reinterprets (first consumer: USB/IP URB traffic).
send pipes raw stdin into the tunnel; recv drains buffered bytes to raw
stdout. The link token is the auth (mirrors drive-poll); the stream resolves
only under the live link generation. Poll-drained at the surface
capability Print the adapter's declared capability (hostable_types)
hint Keyword hints: the full user message arrives on stdin; emit at most one
matched hint line (declaration order, first unseen wins) for the adapter's
context channel. The per-session seen-set fires each hint once per --session (a
/clear = a new session = re-armed). Needs --manifest
shutdown Graceful live-agent signoff: run the final context save BEFORE teardown, then
soft-stop. The spt shutdown lifecycle path
owner-shutdown A shell suspends its linked owner directly, bypassing agent comms — gated by the
manifest can_shutdown pre-consent grant, fail-closed. The firing shell cascades
offline with its siblings, by design
help Print this message or the help of the given subcommand(s)
Options:
--adapter <ADAPTER>
adapter_name — the calling harness adapter. Optional: an explicit name[:profile]
override for adapter dev/iteration. Omitted, listen resolves the owning adapter/profile
at bind from the seed's parent pid (host_binaries → active-profile pointer →
registered_at_ms)
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--manifest <MANIFEST>
Path to the adapter's runtime manifest (when the command needs it)
-h, --help
Print help (see a summary with '-h')
spt api seed
Harness-hosted startup: record an ephemeral seed keyed by parent pid
Usage: spt api seed [OPTIONS] --pid <PID> --session-id <SESSION_ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--pid <PID>
--session-id <SESSION_ID>
-h, --help Print help
spt api listen
Consume a seed and hold the perch + relay loop (blocks)
Usage: spt api listen [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--parent-pid <PARENT_PID> Override the parent-pid anchor (defaults to the self-discovered
PPID)
--once Drain backlog + one receive cycle, then exit (testability)
--subnet <SUBNET> Home subnet for a NEW endpoint (required on a multi-subnet node —
home is assigned at creation, never guessed)
-h, --help Print help
spt api bind
Post-spawn bind of a session to its perch
Usage: spt api bind [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored
by the read/status commands (list, whoami, status,
description, role, the *-list queries, how-to); action
commands ignore it
--set-session-id <BIND_SESSION> The session id discovered post-spawn, written into the perch
record
--subnet <SUBNET> Home subnet for a NEW endpoint (see listen)
--type <ENDPOINT_TYPE> The endpoint type tag (info.json state). Defaults to
live_agent (the agent host); a non-agent endpoint — e.g. a
gateway — binds with its own open-type tag. A revive keeps
the prior type unless this overrides it [default: live_agent]
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's
info.json)
-h, --help Print help
spt api bind-shell
Shell-binary bind: the type=Shell flavor of bind. Resolves the instance by link token alone
(the spawn template carries only {link_token} — "owner from the link") and flips it online. The
credential IS the auth: no token, no bind
Usage: spt api bind-shell [OPTIONS] --link <LINK_TOKEN>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--link <LINK_TOKEN> The link token the broker minted at launch
-h, --help Print help
spt api state
Set activity state busy|idle (also arms the echo-gate sentinel)
Usage: spt api state [OPTIONS] <STATE> <ID>
Arguments:
<STATE> [possible values: busy, idle]
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--no-gate
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api echo-gate
Manage the echo-gate sentinel directly
Usage: spt api echo-gate [OPTIONS] <ACTION> <ID>
Arguments:
<ACTION> [possible values: set, clear]
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api poll
Drain delivered messages (hook channel). With --link this is the shell-flavored relay drain: the
link token is the auth, and the drained rows are the shell's MAC-stamped command/text/file frames
Usage: spt api poll [OPTIONS] <ID>
Arguments:
<ID>
Options:
--include-deferred
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--link <LINK> Shell link token (the relay command-receipt drain)
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api psyche-download
Emit the agent's resume context (durable role/live/project tiers + any not-yet-synthesized
commune/signoff drop as pending slices) to stdout, for the harness adapter's SessionStart hook to
inject as additional context
Usage: spt api psyche-download [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api worker-start
Create a nested worker perch under a parent
Usage: spt api worker-start [OPTIONS] <PARENT> <ID>
Arguments:
<PARENT>
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api worker-stop
Tear down a worker perch
Usage: spt api worker-stop [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api worker-poll
Drain a worker perch's messages
Usage: spt api worker-poll [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api boundary
Rebind the perch to a new session_id, preserving identity (a context clear/compact boundary)
Usage: spt api boundary [OPTIONS] --to-session-id <TO_SESSION> <MODE> <ID>
Arguments:
<MODE> [possible values: clear, compact]
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description,
role, the *-list queries, how-to); action commands ignore it
--to-session-id <TO_SESSION> The new session id to rebind the perch to
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api session-end
Soft teardown (spool/history preserved); --erase hard-wipes
Usage: spt api session-end [OPTIONS] <ID>
Arguments:
<ID>
Options:
--erase
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api presence
Report user/agent presence at this endpoint
Usage: spt api presence [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api driven-by
Report which node (if any) is remote-driving this endpoint
Usage: spt api driven-by [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api history-log
Append normalized history (body on stdin) to the native history store
Usage: spt api history-log [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api digest-entry
Push one digest-record (the published contract JSON line, on stdin) for a log-less adapter —
appended to the perch's digest store, tailed by the session-digest projection
Usage: spt api digest-entry [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api emit
Emit a Shell sensory payload to the owner's live session. REST-only by definition: never
spooled, dropped with a diagnostic when the owner isn't live. The link token is the auth
Usage: spt api emit [OPTIONS] --type <TYPE> --link <LINK> <ID> <PAYLOAD>
Arguments:
<ID>
<PAYLOAD> The sensory payload (descriptive text / encoded blob reference)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--type <TYPE>
--link <LINK> Shell link token (the per-link credential from launch)
-h, --help Print help
spt api drive-poll
Take-and-clear a Shell's pending drive frame: the shell-side drain of the owner→shell ephemeral
control channel. REST-only, exactly-once — the daemon serves the single latest frame and ONLY when
the link matches the slot's write-time stamp (no stale-control replay on relink). The link token is
the auth (mirrors emit). The frame, if any, prints to stdout
Usage: spt api drive-poll [OPTIONS] --link <LINK> <ID>
Arguments:
<ID> The shell instance id (must match the link token's instance)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--link <LINK> Shell link token (the per-link credential from launch)
-h, --help Print help
spt api tunnel
Use the shell end of the opaque byte TUNNEL: a held, reliable-ordered QUIC stream the channel
taxonomy never reinterprets (first consumer: USB/IP URB traffic). send pipes raw stdin into the
tunnel; recv drains buffered bytes to raw stdout. The link token is the auth (mirrors
drive-poll); the stream resolves only under the live link generation. Poll-drained at the surface
Usage: spt api tunnel [OPTIONS] --link <LINK> <ID> <DIRECTION>
Arguments:
<ID> The shell instance id (must match the link token's instance)
<DIRECTION> send (raw stdin → tunnel) or recv (tunnel → raw stdout)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--link <LINK> Shell link token (the per-link credential from launch)
-h, --help Print help
spt api capability
Print the adapter's declared capability (hostable_types)
Usage: spt api capability [OPTIONS]
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the read/status
commands (list, whoami, status, description, role, the *-list queries, how-to); action
commands ignore it
-h, --help Print help
spt api hint
Keyword hints: the full user message arrives on stdin; emit at most one matched hint line
(declaration order, first unseen wins) for the adapter's context channel. The per-session seen-set
fires each hint once per --session (a /clear = a new session = re-armed). Needs --manifest
Usage: spt api hint [OPTIONS] --session <SESSION>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--session <SESSION> The harness session id keying the once-per-session seen-set
-h, --help Print help
spt api shutdown
Graceful live-agent signoff: run the final context save BEFORE teardown, then soft-stop. The `spt
shutdown` lifecycle path
Usage: spt api shutdown [OPTIONS] <ID>
Arguments:
<ID>
Options:
--json Emit machine-readable JSON instead of the human view. Honored by
the read/status commands (list, whoami, status, description, role,
the *-list queries, how-to); action commands ignore it
--token <TOKEN> Capability token proving association to the target perch
--session-id <SESSION_ID> Session id proving association (matches the perch's info.json)
-h, --help Print help
spt api owner-shutdown
A shell suspends its linked owner directly, bypassing agent comms — gated by the manifest
can_shutdown pre-consent grant, fail-closed. The firing shell cascades offline with its siblings,
by design
Usage: spt api owner-shutdown [OPTIONS] --link <LINK> <ID>
Arguments:
<ID> The shell instance id (must match the link token's instance)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--link <LINK> Shell link token (the per-link credential from launch)
-h, --help Print help
spt endpoint
Endpoint operations: list, lifecycle, fork, digest, access.
The noun home for per-endpoint verbs (M8 decision 1). Bare spt endpoint renders the merged listing
— every member subnet's endpoints grouped by subnet, this session's own endpoint pinned distinctly
at the top.
Usage: spt endpoint [OPTIONS] [COMMAND]
Commands:
list Merged endpoint listing (the bare spt endpoint view)
run Bring up an spt-hosted harness endpoint into a broker-held PTY
fork Fork an endpoint into another subnet as a NEW identity
suspend Rest an endpoint cold (the suspend edge)
wake Wake a resting endpoint in place
shutdown Gracefully shut down an agent's own endpoint
stop Soft-stop a perch (spool preserved)
rename Rename an endpoint's logical id across its on-disk state
purge Permanently remove an endpoint and every record keyed on it
digest Show a session's live activity buffer (session digest)
access Endpoint access whitelist for unsolicited off-node inbound
description The endpoint's service-description blurb (ex-resources)
role Show or set the endpoint's durable role — a broad statement of purpose stored in
the mind (tracked/agents/<id>/live-role.md), which replicates with the agent and
renders FIRST at start-transition context injection. Bare role prints the current
role; --overwrite <file> replaces it from a file. This is the sole writer of
the role — no automated path (reconcile / echo-commune / signoff) ever mutates it
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt endpoint list
Merged endpoint listing (the bare spt endpoint view).
Every member subnet's endpoints grouped by subnet, with this session's own endpoint pinned at the
top, AND this node's local perches merged in (so a just-online endpoint not yet advertised still
shows — spt whoami is a thin alias and must see its own perch). --subnet filters the subnet view
to one subnet; --detail adds each endpoint's description blurb (the resource-registry yellow-pages
projection).
Usage: spt endpoint list [OPTIONS]
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--subnet <SUBNET>
Limit the subnet view to one subnet
--detail
Add each endpoint's description blurb to the rows
-h, --help
Print help (see a summary with '-h')
spt endpoint run
Bring up an spt-hosted harness endpoint into a broker-held PTY.
Spawns the adapter's [session.self] command into a broker-owned PTY (the harness self-registers
its perch on bind), then starts / attaches / views per the terminal-action flag. The endpoint id
rides argv so the harness binds to exactly it. This is the non-interactive core (the interactive
picker lands in a later wave); the flags cover every terminal action so a spt-<id> shortcut can
bake a fully non-interactive launch.
Usage: spt endpoint run [OPTIONS]
Options:
--adapter <ADAPTER>
The harness adapter to host: <adapter>[:profile] (must be a registered kind="harness"
adapter on this node). Omit (with --id) to launch the interactive picker
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--id <ID>
The endpoint id to bring up (charset: alphanumeric, -, _). Omit (with --adapter) to
launch the interactive picker
--create
Mint a fresh session (the default; explicit so a non-interactive shortcut can bake
create-vs-resume). Conflicts with --resume
--resume <RESUME>
Resume a prior session id instead of minting a fresh one
--start
Start the endpoint and return immediately (no attach)
--attach
Attach a local terminal after bringup (the default action)
--view
Attach read-only after bringup (watch; forward no input)
--subnet <SUBNET>
Home this endpoint to a named subnet. Required on a node that holds more than one subnet
(the home is assigned at creation and is permanent); the sole subnet is used automatically
when there is one
-h, --help
Print help (see a summary with '-h')
spt endpoint fork
Fork an endpoint into another subnet as a NEW identity.
Home subnets are immutable — fork is the cross-subnet move, never a re-home. Seeds the fork with a
one-time copy of the source's mind (live + project tiers); the two diverge immediately (no ongoing
sync). The source is untouched unless --delete-source. Same-node only today (one node holds one
perch per name, so a local fork needs a new id).
Usage: spt endpoint fork [OPTIONS] --subnet <SUBNET> <SRC> <NEW_ID>
Arguments:
<SRC>
The source endpoint (must exist on this node)
<NEW_ID>
The fork's id (must differ from the source on the same node)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--subnet <SUBNET>
The fork's home subnet — the target (must be a member)
--delete-source
Delete the source endpoint (perch + tracked mind) after the copy
-h, --help
Print help (see a summary with '-h')
spt endpoint suspend
Rest an endpoint cold (the suspend edge).
The resting state machine's suspend edge. From dormant — or straight from active, in which case the
final context save still fires first. Accepts a qualified id@node to suspend an instance on a
paired peer.
Usage: spt endpoint suspend [OPTIONS] <ID>
Arguments:
<ID>
The endpoint id (qualified id@node reaches a paired peer)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt endpoint wake
Wake a resting endpoint in place.
Re-activates the existing seat (state's already there — no fresh spawn), resurfaces undismissed
notifications, and requests an immediate context freshness pull from trusted peers. Accepts a
qualified id@node for an instance on a paired peer.
Usage: spt endpoint wake [OPTIONS] <ID>
Arguments:
<ID>
The endpoint id (qualified id@node reaches a paired peer)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt endpoint shutdown
Gracefully shut down an agent's own endpoint.
Soft-stops the listener, then the suspend edge — the final context save fires and persistent shells
cascade offline with it.
Usage: spt endpoint shutdown [OPTIONS] [ID]
Arguments:
[ID]
The endpoint id (defaults to the session's own perch)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt endpoint stop
Soft-stop a perch (spool preserved).
Removes the ready marker and unregisters the perch; the spool is preserved.
Usage: spt endpoint stop [OPTIONS] <ID>
Arguments:
<ID>
Perch id to stop
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt endpoint rename
Rename an endpoint's logical id across its on-disk state.
Rippled everywhere the id appears: the endpoint's perch dir, its nested companion/worker perches,
and every record naming it. Refuses while the perch is live (stop it first).
Usage: spt endpoint rename [OPTIONS] <OLD_ID> <NEW_ID>
Arguments:
<OLD_ID>
The endpoint's current (bare) id
<NEW_ID>
The new (bare) id — charset-validated; :/@ are reserved
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt endpoint purge
Permanently remove an endpoint and every record keyed on it.
Deletes the perch tree (including its nested companion/worker perches and shells), the registry
address, the endpoint's context branches, and its node-local trust rows. Local only. Offline-only:
refuses while the endpoint is online — stop it first, or pass --force to stop-then-purge.
Irreversible; confirms interactively unless --yes.
Usage: spt endpoint purge [OPTIONS] <ID>
Arguments:
<ID>
The endpoint id to remove
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--yes
Skip the interactive confirmation (for scripts / CI)
--force
Stop the endpoint first if it is online, then purge
-h, --help
Print help (see a summary with '-h')
spt endpoint digest
Show a session's live activity buffer (session digest).
The at-a-glance "what is this agent doing now" view — a projection of the endpoint's normalized
session logs. Pulls a snapshot, or --follows the delta-stream. Local endpoints only.
Usage: spt endpoint digest [OPTIONS] <ID>
Arguments:
<ID>
The (local) endpoint id to read
Options:
--follow
Stream live changes instead of a one-shot snapshot (Ctrl-C to stop)
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--last <LAST>
Show the last N turns instead of the default window (--last 1 is the latest turn — the
turn-end output)
--after <AFTER>
Cursor: show only entries newer than this seq (the authoritative dedup key from a prior
pull). If the seq predates the window, the full window is returned with a predates signal
-h, --help
Print help (see a summary with '-h')
spt endpoint access
Endpoint access whitelist for unsolicited off-node inbound.
Controls which origin nodes may send an endpoint unsolicited off-node inbound. Absent entry = open;
allow flips the endpoint to restricted; revoking the last node leaves it locked down; open
deletes the restriction.
Usage: spt endpoint access [OPTIONS] <COMMAND>
Commands:
allow Whitelist a node for an endpoint (creates the restriction if absent)
revoke Remove a node from an endpoint's whitelist. Never widens: revoking the last node leaves
the endpoint locked down (all unsolicited refused)
open Delete an endpoint's restriction entirely — back to default-open
list List restrictions (all endpoints, or one)
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt endpoint description
The endpoint's service-description blurb (ex-resources).
Bare description shows your own; set authors it. The cross-node projection over every visible
endpoint is endpoint list --detail.
Usage: spt endpoint description [OPTIONS] [COMMAND]
Commands:
set Author this endpoint's blurb (the agent refines its own at runtime; an empty string clears
it back to the node-config seed)
show Show a local endpoint's authored blurb (the bare description view)
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt endpoint role
Show or set the endpoint's durable role — a broad statement of purpose stored in the mind
(tracked/agents/<id>/live-role.md), which replicates with the agent and renders FIRST at
start-transition context injection. Bare role prints the current role; --overwrite <file>
replaces it from a file. This is the sole writer of the role — no automated path (reconcile /
echo-commune / signoff) ever mutates it
Usage: spt endpoint role [OPTIONS]
Options:
--id <ID> Which local endpoint (auto-detected from the session if omitted)
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the
*-list queries, how-to); action commands ignore it
--overwrite <OVERWRITE> Replace the role with the contents of <file> (the only writer)
-h, --help Print help
spt how-to
Task-oriented instructions for agents: how-to <topic>.
The binary's own usage guidance, written for an agent to read and follow. Bare how-to lists the
topics.
Usage: spt how-to [OPTIONS] [TOPIC]
Arguments:
[TOPIC]
The topic to print (omit to list available topics)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt ready
Become reachable: register the perch and listen (blocks).
Drains the spooled backlog first; each received message prints to stdout. With --once, runs a single
drain+receive cycle and exits.
Usage: spt ready [OPTIONS] <ID>
Arguments:
<ID>
This agent's perch id
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--once
Run a single drain+receive cycle, then exit (one-shot fallback for harnesses that cannot
host a long-running listener)
--subnet <SUBNET>
Home subnet for a NEW endpoint (required on a multi-subnet node — home is assigned at
creation, never guessed)
-h, --help
Print help (see a summary with '-h')
spt ring
Send and block for a reply (body read from stdin).
The reply body is printed to stdout; gives up after --timeout seconds.
Usage: spt ring [OPTIONS] <TARGET>
Arguments:
<TARGET>
Target perch id
Options:
--from <FROM>
Sender id (auto-detected from session if omitted)
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
--timeout <TIMEOUT>
Seconds to wait for a reply before giving up
[default: 60]
-h, --help
Print help (see a summary with '-h')
spt send
Send a message (body read from stdin); fire-and-forget
Usage: spt send [OPTIONS] <TARGET>
Arguments:
<TARGET> Target perch id
Options:
--from <FROM> Sender id carried structurally as the message from (auto-detected
from session if omitted)
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the
*-list queries, how-to); action commands ignore it
--idle-only Deliver only when the target is idle (the idle/wake window); hold until
then and never surface to the target's active poll
--active-only Deliver only through the target's own poll (the no-interrupt hook
channel); never wakes an idle target. Replaces the old --deferred
--ephemeral Drop the message if it cannot be delivered in its window, instead of
spooling until delivered
--prefer-native Deliver through the target's translation binary when one is running,
else fall back to the normal channel. Delivers regardless of
idle/active
--force-native Deliver ONLY through the target's translation binary — no fallback and
no spooling. If no binary is running the send is reported undelivered
--json-payload <JSON> Attach an opaque JSON metadata blob alongside the message body, carried
verbatim for the receiving adapter to parse. Does not replace the body
--user-msg Request the user-msg type (the user's authority). Honored only from a
user-backed origin (a Gateway endpoint, or the local user's own CLI);
an agent-family sender is re-stamped to plain msg
-h, --help Print help
spt shell
Shell instances: mint, list, drive, tear down owned surfaces.
The driven surfaces this agent owns. spawn MINTS a new instance identity (<adapter>-<n>) — it is
not the online switch; bringing an existing offline instance back is relink / persistent / wake.
Usage: spt shell [OPTIONS] <COMMAND>
Commands:
spawn Mint a NEW shell instance of a registered kind="shell" adapter: canonical id
<adapter>-<n> (smallest free n; teardown frees slots), starting offline (the launch +
bind handshake brings it online)
list List this owner's instances: canonical id, alias, adapter, status
teardown Destroy an instance (perch removed; mint slot + alias freed)
rename Set/replace an instance's alias (owner-unique)
cmd Drive the shell with a typed capability command (the durable command channel): the op +
positional args are vocabulary-checked against the manifest's [shell.capabilities],
spooled on the shell perch, and drained by the manifest's command_receipt mode (relay
/ stdin)
drive Drive the shell with a typed, EPHEMERAL control payload: the owner→shell mirror of
sensory. The drive-type is vocabulary-checked against [shell.drive], held in a single
latest-wins in-memory slot on the daemon, and drained by the shell's api drive-poll
--link. NEVER spooled — an offline shell drops the payload with a diagnostic (control
is live-or-drop, never replayed)
tunnel Use the shell's opaque byte TUNNEL: a held, reliable-ordered QUIC stream the channel
taxonomy never reinterprets (first consumer: USB/IP URB traffic). send pipes raw stdin
bytes into the tunnel; recv drains buffered bytes to stdout. The shell opts in via
[shell.tunnel]; the tunnel lives for the link (a link-break closes it). Poll-drained
at the surface
send Send a text and/or file payload down the durable 2-way text+file channel (agent→shell;
the shell answers via ordinary spt send). File transfers are progress-queryable by
xfer id
relink Bring an existing offline (persistent) instance back online: re-spawns the binary with a
fresh link token; the perch onlines at its bind
help Print this message or the help of the given subcommand(s)
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
spt shell spawn
Mint a NEW shell instance of a registered kind="shell" adapter: canonical id <adapter>-<n>
(smallest free n; teardown frees slots), starting offline (the launch + bind handshake brings it
online)
Usage: spt shell spawn [OPTIONS] <ADAPTER>
Arguments:
<ADAPTER> The providing shell adapter (must be registered + active)
Options:
--alias <ALIAS> Optional owner-unique friendly label (interchangeable with the canonical id
for addressing; never obscures the adapter)
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--owner <OWNER> Owning endpoint id (auto-detected from session if omitted)
-h, --help Print help
spt shell list
List this owner's instances: canonical id, alias, adapter, status
Usage: spt shell list [OPTIONS]
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--owner <OWNER>
-h, --help Print help
spt shell teardown
Destroy an instance (perch removed; mint slot + alias freed)
Usage: spt shell teardown [OPTIONS] <SHELL_REF>
Arguments:
<SHELL_REF> Canonical id or alias
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--owner <OWNER>
-h, --help Print help
spt shell rename
Set/replace an instance's alias (owner-unique)
Usage: spt shell rename [OPTIONS] <SHELL_REF> <ALIAS>
Arguments:
<SHELL_REF> Canonical id or current alias
<ALIAS> The new alias
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--owner <OWNER>
-h, --help Print help
spt shell cmd
Drive the shell with a typed capability command (the durable command channel): the op + positional
args are vocabulary-checked against the manifest's [shell.capabilities], spooled on the shell
perch, and drained by the manifest's command_receipt mode (relay / stdin)
Usage: spt shell cmd [OPTIONS] <SHELL_REF> [OP]...
Arguments:
<SHELL_REF> Canonical id or alias
[OP]... The capability op + args (vocabulary-checked against the manifest)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--owner <OWNER>
-h, --help Print help
spt shell drive
Drive the shell with a typed, EPHEMERAL control payload: the owner→shell mirror of sensory. The
drive-type is vocabulary-checked against [shell.drive], held in a single latest-wins in-memory
slot on the daemon, and drained by the shell's api drive-poll --link. NEVER spooled — an offline
shell drops the payload with a diagnostic (control is live-or-drop, never replayed)
Usage: spt shell drive [OPTIONS] --type <DRIVE_TYPE> <SHELL_REF> <PAYLOAD>
Arguments:
<SHELL_REF> Canonical id or alias
<PAYLOAD> The opaque control payload (descriptive text / encoded blob reference)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--type <DRIVE_TYPE> The drive payload type (vocabulary-checked against [shell.drive])
--owner <OWNER>
-h, --help Print help
spt shell tunnel
Use the shell's opaque byte TUNNEL: a held, reliable-ordered QUIC stream the channel taxonomy never
reinterprets (first consumer: USB/IP URB traffic). send pipes raw stdin bytes into the tunnel;
recv drains buffered bytes to stdout. The shell opts in via [shell.tunnel]; the tunnel lives for
the link (a link-break closes it). Poll-drained at the surface
Usage: spt shell tunnel [OPTIONS] <SHELL_REF> <DIRECTION>
Arguments:
<SHELL_REF> Canonical id or alias
<DIRECTION> send (raw stdin → tunnel) or recv (tunnel → raw stdout)
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--owner <OWNER>
-h, --help Print help
spt shell send
Send a text and/or file payload down the durable 2-way text+file channel (agent→shell; the shell
answers via ordinary spt send). File transfers are progress-queryable by xfer id
Usage: spt shell send [OPTIONS] <SHELL_REF> [TEXT]
Arguments:
<SHELL_REF> Canonical id or alias
[TEXT] The text payload
Options:
--file <FILE> A file to transfer to the shell
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--owner <OWNER>
-h, --help Print help
spt shell relink
Bring an existing offline (persistent) instance back online: re-spawns the binary with a fresh link
token; the perch onlines at its bind
Usage: spt shell relink [OPTIONS] <SHELL_REF>
Arguments:
<SHELL_REF> Canonical id or alias
Options:
--json Emit machine-readable JSON instead of the human view. Honored by the
read/status commands (list, whoami, status, description, role, the *-list
queries, how-to); action commands ignore it
--owner <OWNER>
-h, --help Print help
spt whoami
Print this session's own perch id.
Resolved from $OWL_SESSION_ID / $SPT_AGENT_ID.
Usage: spt whoami [OPTIONS]
Options:
--json
Emit machine-readable JSON instead of the human view. Honored by the read/status commands
(list, whoami, status, description, role, the *-list queries, how-to); action commands
ignore it
-h, --help
Print help (see a summary with '-h')
Manifest JSON Schema
The machine-readable contract for adapter manifests, served at a stable URL:
https://sabermage.github.io/spt-releases/manifest.schema.json
- Generated from the same code that parses manifests — the schema is
always exactly what
spt adapter addaccepts structurally. It also ships as a release asset with every release. - JSON Schema draft 2020-12; the
$idis the canonical URL above and is stable across releases. - Field doc-comments ride along as
descriptions — the schema doubles as field-level documentation. - Manifests are authored as TOML; the schema describes the equivalent data model (validate the TOML-parsed document).
- Cross-field rules the schema can’t express (kind↔
[shell]agreement, strategy/avenue required fields) are listed in the manifest reference and enforced byspt adapter add.
Example — validate a manifest mechanically (Python, any JSON-Schema validator works the same way):
import json, tomllib, urllib.request, jsonschema
schema = json.load(urllib.request.urlopen(
"https://sabermage.github.io/spt-releases/manifest.schema.json"))
with open("manifest.toml", "rb") as f:
manifest = tomllib.load(f)
jsonschema.validate(manifest, schema) # raises on violation
print("manifest is structurally valid")
Install scripts
The canonical non-interactive installers, served at the permanent URLs:
-
Linux: https://sabermage.github.io/spt-releases/install.sh
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh -
Windows: https://sabermage.github.io/spt-releases/install.ps1
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
What they do
- Resolve the latest release (or
SPT_INSTALL_VERSION). - Download the platform binary and the release’s
SHA256SUMS. - Verify the sha256 — a mismatch refuses before anything is placed.
- Place the binary under the per-OS install root (
~/.local/bin/%LOCALAPPDATA%\spt-core\bin). - Register the user PATH (at most once; idempotent re-runs).
- Print the absolute installed path — on Windows the PATH change reaches new terminals only, so the first invocation in the current one uses that absolute path.
They run unattended (non-interactive by construction — they double as every adapter’s pack-in installer), and re-running is always safe.
Environment knobs
| Env var | Meaning |
|---|---|
SPT_INSTALL_VERSION | Pin a release tag (default: latest) |
SPT_INSTALL_DIR | Override the install directory |
SPT_INSTALL_REPO | Override the source repo (default SaberMage/spt-releases) |
SPT_INSTALL_ASSET_BASE | URL or local dir holding assets + SHA256SUMS directly (CI / air-gap / mirrors) |
SPT_INSTALL_NO_PATH | 1 = skip PATH registration |
Trust model
First fetch: HTTPS + GitHub + sha256 against the release’s SHA256SUMS.
Thereafter spt update performs full Ed25519 verification against the
two-key trust anchor embedded
in the binary. MIT-licensed — copy them into your own bootstrap freely.
OS-service registration
Not yet: the daemon auto-starts on any spt invocation, which covers
dev-stage use. Known gap until then: after a reboot, a node is unreachable
until something on it invokes spt. Service registration ships in a later
release.