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: install spt-core, bring two agents online, and pass real messages between them — including one delivered while the receiver was offline.
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. You need two terminals.
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. Bring an agent online
In terminal B, become reachable as bob:
$ spt poll bob
READY:bob
poll registers a perch for bob (his identity + address on this machine),
drains any backlog, and blocks listening. Leave it running.
3. Send him a message
In terminal A:
$ echo "hello bob - alice here" | spt send bob --from alice
SENT:bob
(Windows PowerShell: "hello bob - alice here" | spt send bob --from alice.)
Terminal B prints it immediately:
__REPLY_TO__:alice
hello bob - alice here
SENT means live delivery — bob was listening, so the message went straight
to his terminal over a local connection. The __REPLY_TO__:alice line is the
routing header: whoever receives this knows where a reply goes.
4. Deliver to someone who’s offline
Stop bob (Ctrl-C in terminal B), then send again from terminal A:
$ echo "ping while you were away" | spt send bob --from alice
QUEUED:bob
QUEUED means bob has a perch but isn’t listening — the message went to his
durable spool instead of being dropped. Bring him back in terminal B:
$ spt poll bob
READY:bob
__REPLY_TO__:alice
ping while you were away
The backlog drains the moment he’s back. Nothing is lost between sessions.
5. What just happened
- Perch — registering as
bobcreated 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 nextpoll/ready. - Reply routing — the
__REPLY_TO__header travels with every message;spt send --reply-to aliceanswers the sender without knowing anything else about them. - No daemon ceremony — you never started a server. Anything that needs the per-machine daemon auto-starts it on demand.
Next
- How-to: block on an answer with
spt ring bob— 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/poll/ring/list— every flag, generated from the binary itself. - Going cross-machine: Networking & pairing
— pair a second machine with a TOTP code and the same
sendreachesbobon the other box.
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
- 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 ·│ │ │ │ alice │ │ doorbell│ │
│ └────────┘ │ lifecycle │ │ │ │(agent)│ │ (shell) │ │
│ └───────────┘ │ │ └───────┘ └─────────┘ │
│ ┌─────┐ ┌─────┐ │ └──────────────────────────────┘
│ │ bob │ │ ling│ ← endpoints (perches live on disk; sessions come
│ └─────┘ └─────┘ and go, identity persists)
└──────────────────────────────┘
Endpoints and perches
An endpoint is anything addressable: an agent (bob), 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: bob is bob 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_TO__ routing 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 bob 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 wake bob 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 & pairing |
| 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 nextpoll/ready. A target with no perch is an error (NO_PERCH) — identity is never invented on someone else’s behalf. - Reply routing. Every message carries a
__REPLY_TO__header;spt send --reply-to <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. - Deferred delivery.
--deferredspools without waking a live listener: for context that should reach the agent at its next natural boundary rather than interrupting now. Deferred messages are also held for resting (dormant/suspended) instances and released exactly once on wake. - 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 (bob) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution refuses and asks you to
qualify (bob@desktop) rather than guessing. The full form is
[subnet:]id[@node].
Commands
send · ring · ready · poll · list · stop · whoami — every flag
in the CLI reference.
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). - 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 shutdown/api shutdown). The echo-commune fires before teardown, always. - Echo-commune — sessions that end without a signoff don’t lose 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 suspend or
on attention-shift. Resting instances stay addressable; deferred messages
are held and released exactly once on wake (spt wake). Every
active→resting edge fires a transition echo so the final context delta
lands before the lights go out.
Commands
spt shutdown · suspend · wake · the api lifecycle calls
(reference).
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 — with adapter-supplied
[pty_digest]patterns,spt digest <id>shows an at-a-glance parsed view of what a session is doing now (--followstreams changes). Raw bytes and parsed digest are two channels over the same session surface. - 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 digest · the attach surface · spt api injection-adjacent calls.
Deeper tutorial coming with the docs’ next tier.
Networking & pairing
Zero-config, no-central-server connectivity between your machines. Pair two
nodes once with a six-digit code; from then on, the same spt send bob works
whether bob 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).
- Subnets — machines pair 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 unpaired nodes, ever.
- Pairing — a one-time ceremony seeded by a TOTP code: run
spt pair show-totpon a member node (it also prints anotpauth://URI — put the seed in your authenticator app), type the subnet name + current six digits on the joiner. 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). - 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.
- Resource registry — endpoints may advertise a free-text service blurb
(
spt resources set/list) — an agent yellow-pages over visible rows only.
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 paired substrate.
Commands
spt pair · spt resources · the qualified addressing forms
([subnet:]id[@node]) — CLI reference.
Pairing walkthrough tutorial coming with the docs’ next tier.
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.
- 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.
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.
[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 every api call carries
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"]
| Field | Required | Meaning |
|---|---|---|
name | yes | Adapter id; --adapter <name> on every machinery call |
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 |
[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 {adapter_name}"
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) · 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).
[session.psyche_init]
command = "my-harness run --agent psyche --name {session_name} --model cheap"
cwd = "{psyche_dir}"
env_remove = ["MY_HARNESS_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"
detach = true
keys = ["session_name", "psyche_dir"]
| 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.
[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"]
[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.
[pty_digest] — live activity buffer patterns
For sessions spt-core hosts in its own terminal layer, the adapter may supply
regex patterns that segment the raw PTY byte stream into an at-a-glance
activity digest (spt digest <id>). The patterns are opaque to spt-core —
the same rule as command templates; spt-core compiles and runs them, never
understanding harness semantics.
[pty_digest]
input_pattern = "\\nUSER> " # user-turn boundary (required)
agent_pattern = "\\nAGENT> " # agent-turn boundary (required)
tool_patterns = [ # optional: one regex per known tool,
"(?P<name>Write)\\((?P<arg>[^)]*)\\)", # naming `arg` (+ optional `name`)
]
catchall_pattern = "(?P<name>[A-Z][a-zA-Z]+)\\((?P<arg>[^)]*)\\)" # optional
window_turns = 3 # optional presentation override
persist = false # opt-in coarse activity log (default off)
Both boundary markers are required when the section is present; an absent section just means the digest is unavailable for this adapter.
[update] — adapter self-update
How spt-core updates (and first installs — install is the first update) this adapter:
[update]
avenue = "delegated" # "delegated" | "file_pull"
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`
| 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 |
With file_pull, you sign your 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
notify = { args = ["title", "body"] }
clear = {}
[shell.sensory] # the shell->agent sensory vocabulary
types = ["event"]
The capability and sensory vocabularies live in the manifest — spt-core
resolves them by adapter name, validates agent commands 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)
and pushes sensory payloads with spt api … emit.
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;kind = "harness"must not have one.[history] strategy = "fetcher"requiresfetcher;locate_normalizerequires bothlocate_templateandnormalize_command.[env.*] direction = "inject"requires avalue.[update] avenue = "delegated"requirescommand;file_pullrequiresrepoandsigning_key.[pty_digest], when present, requires non-emptyinput_patternandagent_pattern.
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 every api call:
--adapter <name>is mandatory. Every machinery invocation names the calling adapter — that’s what keeps a multi-harness node unambiguous.- 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> [--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 at
creation; spt-core never guesses).
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.
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 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 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 will never prompt), 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)
After first install, spt-core keeps itself current (signed self-update) — the bootstrap never needs to handle upgrades.
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\bin\spt.exe'
} else {
$spt = 'spt'
}
& $spt --version
The Windows PATH-refresh gotcha
The installer registers the binary directory on the user PATH via the registry. Already-running processes — including the terminal (and your bootstrap) that just ran the installer — do not see registry PATH changes.
So: the first invocation after an install must use the absolute path
(%LOCALAPPDATA%\spt\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 take environment knobs — never flags, 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 — the installer never needs to be the strong link twice.
Instances
One endpoint, several seats. bob is a single identity; an instance of
bob 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 rename) ripples everywhere, collision-checked. - Bare-id resolution never guesses —
bobresolves locally first, then to the sole live instance; with several live nodes (or several subnets) it refuses and makes you qualify (bob@desktop,home:bob). 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 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 suspend bob@desktop/spt wake bob@desktopwork across paired nodes.
Commands
spt list · rename · fork · suspend · wake · resources —
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.
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. - 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.
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 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). - 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.
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 <COMMAND>
Commands:
send Send a message (body read from stdin). Fire-and-forget
ring Send and block for a reply (body read from stdin; reply printed to stdout)
ready Become reachable: register the perch and listen for messages (blocks)
poll Receive messages: drain backlog then listen, printing each to stdout
list List this node's perches (id, state, address, ready, alive)
stop Soft-stop a perch: remove the ready marker + unregister (spool preserved)
whoami Print this session's own perch id (from $OWL_SESSION_ID / $SPT_AGENT_ID)
pair Subnet pairing: show this node's current pairing code (and mint new subnets). Pairing is how machines join a private subnet
digest Show a hosted session's live activity buffer (PTY digest) — the at-a-glance "what is this agent doing now" view. Pulls a snapshot, or `--follow`s the delta-stream. Local endpoints only
rename Rename an endpoint's logical id, rippled across its on-disk state: the endpoint's perch dir, its nested companion/worker perches, and every record naming it. Refuses while the perch is live (stop it first)
access Endpoint access whitelist: 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
grant Consent grant store: which agents hold which gated capabilities on this node. Default-deny (the access whitelist's opposite polarity). An ungranted ask escalates interactively; `add` is the durable allow-always answer
adapter Adapter registration: the node-local registered set — what this node can drive/launch (one command for harness and shell adapters). The registered set feeds creation-time adapter selection, shell discovery, and the self-update ripple
shell Shell instances: mint, list, drive, and tear down 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
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
notif Inspect and acknowledge notifications. Dismissal is the explicit ack — it latches and replicates subnet-wide
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
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)
resources The subnet resource registry: per-endpoint free-text service blurbs — an agent yellow-pages. `set` authors your own, `show` reads a local endpoint's, `list` projects the registry view over visible rows
suspend Rest an endpoint cold: 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
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
shutdown Gracefully shut down an agent's own endpoint: soft-stop the listener, then the suspend edge — the final context save fires and persistent shells cascade offline with it
api Harness-contract inbound surface: the entry points a harness's hooks fire to keep spt-core's on-disk state in sync
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
spt send
Send a message (body read from stdin). Fire-and-forget
Usage: spt send [OPTIONS] [TARGET]
Arguments:
[TARGET] Target perch id. May be omitted when --reply-to is given
Options:
--from <FROM> Sender id written into __REPLY_TO__ (auto-detected from session if omitted)
--reply-to <REPLY_TO> Reply to this sender; sets the target and labels output REPLIED
--deferred Spool only (deferred / hook channel) — no live wake
-h, --help Print help
spt ring
Send and block for a reply (body read from stdin; reply printed to stdout)
Usage: spt ring [OPTIONS] <TARGET>
Arguments:
<TARGET> Target perch id
Options:
--from <FROM> Sender id (auto-detected from session if omitted)
--timeout <TIMEOUT> Seconds to wait for a reply before giving up [default: 60]
-h, --help Print help
spt ready
Become reachable: register the perch and listen for messages (blocks)
Usage: spt ready [OPTIONS] <ID>
Arguments:
<ID> This agent's perch id
Options:
--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 poll
Receive messages: drain backlog then listen, printing each to stdout
Usage: spt poll [OPTIONS] <ID>
Arguments:
<ID> This agent's perch id
Options:
--once Run a single drain+receive cycle, then exit (legacy one-shot)
--subnet <SUBNET> Home subnet for a NEW endpoint (see `ready`)
-h, --help Print help
spt list
List this node's perches (id, state, address, ready, alive)
Usage: spt list
Options:
-h, --help Print help
spt stop
Soft-stop a perch: remove the ready marker + unregister (spool preserved)
Usage: spt stop <ID>
Arguments:
<ID> Perch id to stop
Options:
-h, --help Print help
spt whoami
Print this session's own perch id (from $OWL_SESSION_ID / $SPT_AGENT_ID)
Usage: spt whoami
Options:
-h, --help Print help
spt pair
Subnet pairing: show this node's current pairing code (and mint new subnets). Pairing is how machines join a private subnet
Usage: spt pair <COMMAND>
Commands:
show-totp Show a subnet's current 6-digit pairing code + `otpauth://` URI — gated behind OS elevation (or read the code from your authenticator app). With no flag the node's sole subnet is used; if it holds several, `--subnet <name>` is required (never guessed). `--create-new <name>` mints a fresh subnet first
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
spt pair show-totp
Show a subnet's current 6-digit pairing code + `otpauth://` URI — gated behind OS elevation (or read the code from your authenticator app). With no flag the node's sole subnet is used; if it holds several, `--subnet <name>` is required (never guessed). `--create-new <name>` mints a fresh subnet first
Usage: spt pair show-totp [OPTIONS]
Options:
--subnet <SUBNET> Which subnet's code to show. Required only when the node holds several
--create-new <NAME> Mint a fresh subnet with this name (this node becomes the sole seed-holder), then show its code. Mutually exclusive with `--subnet`
-h, --help Print help
spt digest
Show a hosted session's live activity buffer (PTY digest) — the at-a-glance "what is this agent doing now" view. Pulls a snapshot, or `--follow`s the delta-stream. Local endpoints only
Usage: spt 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 structured JSON instead of the human-glanceable render
-h, --help Print help
spt rename
Rename an endpoint's logical id, rippled across its on-disk state: 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 rename <OLD_ID> <NEW_ID>
Arguments:
<OLD_ID> The endpoint's current (bare) id
<NEW_ID> The new (bare) id — charset-validated; `:`/`@` are reserved
Options:
-h, --help Print help
spt access
Endpoint access whitelist: 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 access <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:
-h, --help Print help
spt access allow
Whitelist a node for an endpoint (creates the restriction if absent)
Usage: spt access allow <ENDPOINT> <NODE>
Arguments:
<ENDPOINT> The endpoint to restrict
<NODE> The origin node's pubkey hex (as shown by `spt list` / pairing)
Options:
-h, --help Print help
spt access revoke
Remove a node from an endpoint's whitelist. Never widens: revoking the last node leaves the endpoint locked down (all unsolicited refused)
Usage: spt access revoke <ENDPOINT> <NODE>
Arguments:
<ENDPOINT>
<NODE>
Options:
-h, --help Print help
spt access open
Delete an endpoint's restriction entirely — back to default-open
Usage: spt access open <ENDPOINT>
Arguments:
<ENDPOINT>
Options:
-h, --help Print help
spt access list
List restrictions (all endpoints, or one)
Usage: spt access list [ENDPOINT]
Arguments:
[ENDPOINT]
Options:
-h, --help Print help
spt grant
Consent grant store: which agents hold which gated capabilities 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 <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:
-h, --help Print help
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:
--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:
--qualifier <QUALIFIER>
-h, --help Print help
spt grant list
List grant rows (all, or one agent's)
Usage: spt grant list [AGENT]
Arguments:
[AGENT]
Options:
-h, --help Print help
spt adapter
Adapter registration: the node-local registered set — what this node can drive/launch (one command for harness and shell adapters). The registered set feeds creation-time adapter selection, shell discovery, and the self-update ripple
Usage: spt adapter <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)
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
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)
Options:
--github <GITHUB> GitHub source `user/repo` — manifest-first, then install via the declared `[update]` avenue
-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
-h, --help Print help
spt adapter list
List registered adapters (active and soft-deregistered)
Usage: spt adapter list
Options:
-h, --help Print help
spt shell
Shell instances: mint, list, drive, and tear down 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 <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)
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:
-h, --help Print help
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)
--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:
--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:
--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:
--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:
--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
--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:
--owner <OWNER>
-h, --help Print help
spt 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
Usage: spt notify [OPTIONS] [BODY]
Arguments:
[BODY] Notification body (read from stdin when omitted)
Options:
--subnet <SUBNET> Which subnet to notify. Required only when the node holds several (never guessed)
--from <FROM> Issuer endpoint id (auto-detected from session if omitted)
-h, --help Print help
spt notif
Inspect and acknowledge notifications. Dismissal is the explicit ack — it latches and replicates subnet-wide
Usage: spt notif <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:
-h, --help Print help
spt notif list
List notifications (all member subnets, or one)
Usage: spt notif list [OPTIONS]
Options:
--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 <NOTIF_ID>
Arguments:
<NOTIF_ID> The notif id (as shown by `spt notif list`)
Options:
-h, --help Print help
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 <COMMAND>
Commands:
apply Apply the staged, verified self-update now
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
spt update apply
Apply the staged, verified self-update now
Usage: spt update apply
Options:
-h, --help Print help
spt 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 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:
--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
spt resources
The subnet resource registry: per-endpoint free-text service blurbs — an agent yellow-pages. `set` authors your own, `show` reads a local endpoint's, `list` projects the registry view over visible rows
Usage: spt resources <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
list The subnet resource-registry projection: `(id, node, status, blurb)` over VISIBLE rows only (hidden endpoints never appear — discovery leaks nothing a viewer couldn't reach)
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
spt resources set
Author this endpoint's blurb (the agent refines its own at runtime; an empty string clears it back to the node-config seed)
Usage: spt resources set [OPTIONS] <TEXT>
Arguments:
<TEXT> The blurb text ("" clears)
Options:
--id <ID> Which local endpoint (auto-detected from the session if omitted)
-h, --help Print help
spt resources show
Show a local endpoint's authored blurb
Usage: spt resources show [ID]
Arguments:
[ID] The local endpoint id (auto-detected if omitted)
Options:
-h, --help Print help
spt resources list
The subnet resource-registry projection: `(id, node, status, blurb)` over VISIBLE rows only (hidden endpoints never appear — discovery leaks nothing a viewer couldn't reach)
Usage: spt resources list [OPTIONS]
Options:
--subnet <SUBNET> Limit to one subnet (all member subnets otherwise)
-h, --help Print help
spt suspend
Rest an endpoint cold: 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 suspend <ID>
Arguments:
<ID> The endpoint id (qualified `id@node` reaches a paired peer)
Options:
-h, --help Print help
spt 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 wake <ID>
Arguments:
<ID> The endpoint id (qualified `id@node` reaches a paired peer)
Options:
-h, --help Print help
spt shutdown
Gracefully shut down an agent's own endpoint: soft-stop the listener, then the suspend edge — the final context save fires and persistent shells cascade offline with it
Usage: spt shutdown [ID]
Arguments:
[ID] The endpoint id (defaults to the session's own perch)
Options:
-h, --help Print help
spt api
Harness-contract inbound surface: the entry points a harness's hooks fire to keep spt-core's on-disk state in sync
Usage: spt api [OPTIONS] --adapter <ADAPTER> <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
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
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
capability Print the adapter's declared capability (`hostable_types`)
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. Required on every `api` call
--manifest <MANIFEST> Path to the adapter's runtime manifest (when the command needs it)
-h, --help Print help
spt api seed
Harness-hosted startup: record an ephemeral seed keyed by parent pid
Usage: spt api --adapter <ADAPTER> seed --pid <PID> --session-id <SESSION_ID>
Options:
--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 --adapter <ADAPTER> listen [OPTIONS] <ID>
Arguments:
<ID>
Options:
--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 --adapter <ADAPTER> bind [OPTIONS] <ID>
Arguments:
<ID>
Options:
--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`)
--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 --adapter <ADAPTER> bind-shell --link <LINK_TOKEN>
Options:
--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 --adapter <ADAPTER> state [OPTIONS] <STATE> <ID>
Arguments:
<STATE> [possible values: busy, idle]
<ID>
Options:
--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 --adapter <ADAPTER> echo-gate [OPTIONS] <ACTION> <ID>
Arguments:
<ACTION> [possible values: set, clear]
<ID>
Options:
--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 --adapter <ADAPTER> poll [OPTIONS] <ID>
Arguments:
<ID>
Options:
--include-deferred
--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 worker-start
Create a nested worker perch under a parent
Usage: spt api --adapter <ADAPTER> worker-start [OPTIONS] <PARENT> <ID>
Arguments:
<PARENT>
<ID>
Options:
--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 --adapter <ADAPTER> worker-stop [OPTIONS] <ID>
Arguments:
<ID>
Options:
--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 --adapter <ADAPTER> worker-poll [OPTIONS] <ID>
Arguments:
<ID>
Options:
--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 --adapter <ADAPTER> boundary [OPTIONS] --to-session-id <TO_SESSION> <MODE> <ID>
Arguments:
<MODE> [possible values: clear, compact]
<ID>
Options:
--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 --adapter <ADAPTER> session-end [OPTIONS] <ID>
Arguments:
<ID>
Options:
--erase
--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 --adapter <ADAPTER> presence [OPTIONS] <ID>
Arguments:
<ID>
Options:
--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 --adapter <ADAPTER> driven-by [OPTIONS] <ID>
Arguments:
<ID>
Options:
--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 --adapter <ADAPTER> history-log [OPTIONS] <ID>
Arguments:
<ID>
Options:
--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 --adapter <ADAPTER> emit --type <TYPE> --link <LINK> <ID> <PAYLOAD>
Arguments:
<ID>
<PAYLOAD> The sensory payload (descriptive text / encoded blob reference)
Options:
--type <TYPE>
--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 --adapter <ADAPTER> capability
Options:
-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 --adapter <ADAPTER> shutdown [OPTIONS] <ID>
Arguments:
<ID>
Options:
--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 --adapter <ADAPTER> owner-shutdown --link <LINK> <ID>
Arguments:
<ID> The shell instance id (must match the link token's instance)
Options:
--link <LINK> Shell link token (the per-link credential from launch)
-h, --help Print help
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\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 never prompt (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.