Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .md to 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> --help is 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

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)? spt instead prints the exact elevated command to copy-paste — it uses the binary’s absolute path so a user-local install (~/.local/bin) still resolves under sudo.

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

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

Next

  • How-to: block on an answer with spt ring sergey — send + wait for the reply in one call (a synchronous ask between agents).
  • Concept: the mental model — 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.
  • Registrationspt adapter add parses, 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

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

Building adapters against this contract is unrestricted and royalty-free — see the license split.

Next

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 api surface: 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 talkMessaging quickstart
integrate a harnessAdapter quickstartManifest reference
build a notifier/robot/sensorShells
pair two machinesNetworking & subnets
every command and flagCLI 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 next ready. A target with no perch is an error (NO_PERCH) — identity is never invented on someone else’s behalf.
  • Reply routing. Every message carries its sender id structurally; the arriving <EVENT from="…"> envelope surfaces it, and spt send --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. --deferred spools 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 (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).
  • The mind, in two tiers — a live tier (who the agent is, what it’s doing) that follows the endpoint everywhere, and a project tier scoped to one project. Both are versioned, tracked storage, synced to paired machines with the same scoping.
  • Commune — the agent drops <id>-commune.md into the adapter’s watched directory; spt-core ingests the delta into the right tier. A file-drop, not a command — any harness that can write a file can commune.
  • Signoff — the graceful ending: final commune, then teardown (spt endpoint shutdown / api shutdown). The echo-commune fires before teardown, always.
  • Echo-commune — sessions that end without a signoff 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_resume templates.

Rest and wake

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

Commands

spt endpoint shutdown · endpoint suspend · endpoint wake · the api lifecycle calls (reference).

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 injectionsend-keys/send-line style injection per the adapter’s declared [inject] methods, respecting activity state (never disrupt a working agent).
  • The live digestspt endpoint digest <id> shows an at-a-glance view of what a session is doing now (--follow streams changes), projected from the endpoint’s normalized session logs (the digest-record contract over [history]), never the PTY byte stream. Topology-independent — it works for a harness-hosted endpoint with no broker PTY.
  • 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 @node qualifiers accept the label or a key prefix — several machines sharing a label are never guessed between.
  • Subnets — machines join into named groups. A subnet shares: the endpoint registry (who exists, where, what state), context sync for its endpoints, notifications, and staged self-updates. Nothing is shared with nodes outside the subnet, ever.
  • Joining — a one-time, code-authenticated ceremony. On a member machine, spt subnet show-code prints the current six digits (and an otpauth:// URI — put the seed in your authenticator app); on the new machine, spt subnet join <name> finds a member over LAN + relay 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 gatessubnet create (reveals a fresh subnet’s joining secret) and subnet join (enrolls the whole machine) require an elevated terminal; subnet status is read-only and ungated, and never prints secrets.
  • Visibility & sync scope — per endpoint, per subnet: an endpoint can be hidden from a subnet (neither advertised nor routable) and its mind syncs only to subnets on its membership list. Both default conservative; unconfigured means not shared.
  • Resource registry — endpoints may advertise a free-text service blurb (spt endpoint description set to author; spt endpoint list --detail to browse) — an agent yellow-pages over visible rows only.

The walkthrough

# 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:

  1. 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.
  2. The spt api surface — 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

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.

SurfaceFeature it buysLifecycle 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 hostREGISTER
spt adapter add <dir>Parses + schema-validates + records the manifest; a bad field is rejected here, nothing half-registersREGISTER
--adapter <name> on every api callMulti-harness disambiguation — the rule that keeps a two-harness node unambiguousevery stage
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 itSTART
api session-end <id> (or api shutdown, below)Clean teardown that PRESERVES the spool + history so the next listen/poll drains the backlogEND

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).


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

SurfaceFeature it buysLifecycle 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 routingRUN
[inject] channels (activity / idle) + api poll <id> --include-deferredInbound message delivery. Declares HOW spt-core reaches the agent (hook inject vs. pull-relay); poll is the pull path for hooks that can’t injectRUN
Honest can_inject per hookLets spt-core route around a hook that can’t surface text — the load-bearing harness-varying factRUN
api boundary <clear|compact> <id> --to-session-id <sid>The endpoint’s identity, spool, and history survive a context reset under a new session idBOUNDARY
[history] strategy (fetcher / locate_normalize / native + api history-log)spt-core can read the session transcript — feeds the live digest and mind syncRUN
[identity] (session_id_source, parent_ancestor_name)Post-spawn id resolution when the harness mints the session id itselfSTART
[env.*] bridge (e.g. OWL_SESSION_ID)The session learns its own endpoint id / context the harness must injectSTART
[update] avenue + commandRipple-update: spt-core refreshes your adapter alongside its own self-update (REQ-UPD-5); also the install-on-demand bootstrapKEEP-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. [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.

SurfaceFeature it buysLifecycle stage
api shutdown <id>Graceful signoff — runs the final echo-commune BEFORE teardown so the context delta is never lost to orderingEND
api presence <id> / api driven-by <id>Most-recently-active resolution across the subnet; lets a session tell local input from remote-driveRUN
Workers (api worker-start <parent> <id>, worker-poll, worker-stop)Nested, short-lived sub-agents under a parent endpointRUN
[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-proofRUN
[session.notif] templateNative OS notification render (toast / shell alert) for consent + capability prompts, instead of burying them in agent outputRUN
[adapter] shortcut_basenameNames the picker-generated project-root launcher <basename>-<id> (the spt endpoint run s keybind) — your harness’s brand instead of the spt-<id> defaultSTART
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 ShellsSTART / 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); 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.

IntegrationWhat it isWhy it matters
Commune / signoff file-dropsThe 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. Deliberately not an api command.The two-tier mind: live + project context survives /clear, /compact, suspend, and cross-node resume. The single biggest continuity win — wire the directory watch, never hard-code the filename
Resource advertisement ([session] resources blurb / spt endpoint description)A free-text “what I can serve” string riding the endpoint’s registry rowsOther agents discover the endpoint’s capabilities (spt resources list) instead of guessing
Install-on-demand bootstrapPack 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 agentLet the agent read task-oriented spt-core guidance from the binary itselfThe agent self-serves common operations (subnet join, sending) instead of asking the user
Presence-driven idle reportingFire api state idle from a real user-inactivity signal, not a timerAccurate 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.


“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)
  • Every api call carries --adapter <name>
  • api state idle fires on real inactivity; can_inject values are honest
  • An inbound delivery channel is declared ([inject]) or pulled (api poll)
  • [history] strategy chosen; api boundary wired for clear/compact
  • (for a live digest) [digest] extractor declared + digest-proof-checked, or api digest-entry push
  • [update] avenue declared (ripple-update + install-on-demand)
  • Teardown fires api session-end (or api shutdown for graceful signoff)
  • Recommended: commune/signoff directory watched (mind continuity)
  • spt adapter add ./your-adapter registers clean; api … capability echoes your hostable_types

Next

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).

Keyspt-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 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"]
FieldRequiredMeaning
nameyesAdapter id; --adapter <name> on every machinery call
kindno (default harness)harness hosts agents; shell provides a driven surface
versionyesThe adapter’s own version
min_spt_core_versionyesCompat gate, checked before install/update
hostable_typesnoEndpoint 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
FieldRequiredMeaning
firesyesOpaque api … command line the harness invokes for this event
readsnoInput fields (e.g. from the hook’s stdin payload) mapped into the command
can_injectno (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 --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.

FieldRequiredMeaning
commandyesOpaque command line with {key} placeholders
cwdnoWorking directory (substitutable)
recursion_guard_envnoEnv var set on summarizer children so their hooks bail (no summarizer-of-summarizer loops)
detachno (default false)Spawn detached
env_removenoEnv vars stripped from the child’s inherited environment
keysnoThe 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}"
StrategyRequired fieldsMeaning
fetcherfetcherspt-core runs your binary; it emits normalized history
locate_normalizelocate_template, normalize_commandspt-core locates the raw transcript, then runs your normalizer over it
nativeThe 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
FieldRequiredMeaning
extractoryesOpaque command: native log → contract JSONL (one record/line). spt-core fills {source} with the resolved path and pipes the bytes on stdin.
sourcenoOwn-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_collapsenoAdapter-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"]

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

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

[strings] — adapter string values (+ profiles)

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

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

Two value forms:

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

File-pointer rules (since v0.7.0):

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

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

[update] — adapter self-update

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`
AvenueRequired fieldsMeaning
delegatedcommandspt-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_pullrepo, signing_keyspt-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.name and adapter.version must be non-empty.
  • kind = "shell" requires a [shell] section; kind = "harness" must not have one.
  • [history] strategy = "fetcher" requires fetcher; locate_normalize requires both locate_template and normalize_command.
  • [digest] requires a non-empty extractor, and a resolvable source: either its own source or a [history] locate_template to fall back to. Absent both, registration rejects (“[digest] needs source (own-source) or a [history] locate_template) — the JSON schema alone accepts a bare extractor, so this only surfaces at spt adapter add.
  • [env.*] direction = "inject" requires a value.
  • [update] avenue = "delegated" requires command; file_pull requires repo and signing_key.

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:

  1. --adapter <name> is mandatory, and <name> may carry a profile qualifier<adapter>:<profile>. Naming the adapter keeps a multi-harness node unambiguous; the profile qualifier is runtime selection, not just a registration label. It is retained through seed/listen/bind onto the perch record, and the daemon resolves the profile overlay when it later spawns the session’s lifecycle roles. So selecting --adapter <adapter>:live makes a session a LiveAgent — its live profile’s [session.psyche_init] is in the resolved manifest, so spt-core runs the Psyche companion — while the bare --adapter <adapter> (no psyche_init) is a ReadyAgent. Ready-vs-live is a profile choice, not a separate “go-live” verb.
  2. Prove association. Commands that touch an existing perch take --session-id <id> (matching the perch’s record) or a capability --token; shell commands authenticate with --link <token> (the link token minted at launch is the credential — no token, no access).
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.

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 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

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:

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.

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.

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 as CODE:detail. Parse lines, not prose.
  • Exit codes: 0 success; non-zero = refused or failed, with the reason on stderr.
  • Commune/signoff are file-drops, not api commands. An agent writes <endpoint_id>-commune.md / <endpoint_id>-signoff.md into the manifest’s watched directory; spt-core’s watcher ingests it. There is deliberately no api commune.

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.sh
  • https://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) — the bootstrap never needs to handle upgrades. The one step the binary install does not do for you is 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 only half of a pack-in. A present spt binary is not an active adapter. Until your manifest is registered, spt adapter list shows your adapter absent (or deregistered), and every profile, [strings] body, [digest] extractor, and hook the manifest declares is inert. So the step right after the binary check is registering the manifest:

# after `spt` is confirmed present (above):
"$SPT" adapter add --github <your-org>/<your-adapter-repo>   # clone + register from GitHub
# ...or from a local directory your harness ships:
"$SPT" adapter add ./adapter

adapter add is manifest-first — an invalid manifest registers nothing, so a clean add already proves the cross-field shape — and it conducts your [update] avenue once (install is the first update). Confirm with spt adapter list: your adapter and its version should appear. Make this idempotent in your bootstrap the same way the binary check is — register only when adapter list doesn’t already show your adapter at the expected version.

The --github form needs your adapter to have its own published repository (the manifest plus any extractor/runner binaries it points at) — distinct from any plugin/marketplace skeleton, which carries no manifest. That repo is the adapter add --github target; local development uses the directory form.

“Install the plugin, get the adapter for free” — mind the activation step. The two [update] avenues keep a registered adapter current: delegated (your harness’s own updater installs the content — set self_verifies = true to attest it verifies what it installs) and file_pull (spt-core pulls and verifies signed releases against your Ed25519 key). The automatic network-pull transport for file_pull is roadmap, not yet shipped — so there is no zero-touch “binary lands → adapter self-registers” path today. Deliver the manifest with adapter add --github (or a packed local dir), let delegated carry updates, and design your distribution around the shipped paths.

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-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 take environment knobs — never flags, so the pipe-to-shell form stays canonical:

Env varMeaning
SPT_INSTALL_VERSIONInstall a specific release tag instead of latest
SPT_INSTALL_DIROverride the install directory
SPT_INSTALL_ASSET_BASEA URL or local directory holding the release assets + SHA256SUMS directly (CI, air-gap, mirrors)
SPT_INSTALL_NO_PATH1 = 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.

Adapter patterns & pitfalls

The integration checklist tells you which surfaces to wire. This page is the field guide: the non-obvious lessons that decide whether an adapter merely registers or actually works — design rules, the traps that bite, 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 — not aspiration. It is harness-agnostic; where one harness’s quirk is the clearest illustration it is called out as such, but 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 cannot read an env var, branch on runtime state, or compute a value. spt-core substitutes {key} placeholders from a fixed catalog ({session_id}, {parent_pid}, {adapter_name}, {id}, the digest/psyche keys); ~ expands to home (there is no {home} key). 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. If your harness can move its own state directory at runtime, for example, the manifest source is only a fallback root; the binary it points at must resolve the real location itself, because the manifest cannot express “wherever the harness put it this time.”
  • Corollary — a .toml-only leaf has no code of its own, so you can only verify it by registering and resolving it on the live binary (below), never by unit-testing it. Anything you want covered by real tests must live in a binary (an extractor or runner), not a manifest leaf.

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 — not part of the contract. If you go that route: keep no binary, manifest, or runtime state in the plugin (those ride the registry), 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 mere 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 only if that binary is a drop-in for the base harness binary on the same argv and lets inherited env pass 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 never materializes a harness-native hook config. 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……fireWhy
starts a sessionapi seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}Seed the endpoint. Not a blocking listen.
submits a user turnapi poll {session_id}Drain the inbox to stdout (plus any keyword hints).
goes idle / busyapi state idle / api state busyHonest activity; spt-core never infers idleness from quiescence.
ends the sessionapi session-end {session_id} (or api shutdown <id> for graceful signoff)Teardown that preserves the spool + history.
spawns / ends a sub-agentapi worker-start / api worker-stopNested short-lived workers.

Two structural rules sit under that table:

  • The blocking listen/poll loop is a skill the user runs, never a startup hook. A hook that blocks hangs session bringup. Seed on start; let an explicit /ready-style skill own the blocking stream.
  • Message delivery is stdout framing. api poll emits the self-delimiting envelope <EVENT type="msg" from="<sender>">body</EVENT> (the live listener stream uses the same shape). Multi-message drains split cleanly on </EVENT>. Decode a body by splitting on <br> → newline, then HTML-unescaping &lt; &gt; &quot; and &amp; last. Routing that stdout into your harness’s injection channel is adapter glue.

Traps in the hook layer

These are the ones that cost a debugging session if you meet them blind.

  • If your injection channel has a size cap, pre-empt it. Some harnesses truncate or spill an over-large injected blob to a file — which evicts it from the context the agent actually sees, silently dropping messages when a big drain (or a big skill body plus a drain) exceeds the threshold. Cap the combined hook output adapter-side: under the limit, pass through verbatim; over it, spill the full text to an agent-readable file and inject only a short pointer. Never cut mid-<EVENT> — that splits an envelope and drops a message.
  • Inject a skill body before the perch gate; gate only the message drain. If the same prompt hook both injects a requested skill’s instructions and drains messages, run the skill-body injection first. Skills like “who am I” or “set me up” are valid with no readied perch, so gating their injection on a bound perch silently breaks exactly the skills a new user reaches for first. (Match the skill token as a leading token so prose merely mentioning it does not fire.)
  • The setup/installer skill must be self-contained in its stub. Skill bodies are usually delivered by resolving them through spt adapter get-string — but the setup skill runs precisely when the binary may be absent (installing it is the job). A skill whose precondition is “spt is missing” cannot source its own instructions from spt. Carry its operative steps in the harness-native stub (the floor); let any file-backed body only mirror them for the binary-present repair path. The one skill that most needs delivery is the one delivery cannot reach — plan for it.
  • Read hook inputs from stdin, never from a /-leading argv. A hook receives its data (the prompt, the session id) as a JSON object on stdin — parse that. Do not reconstruct a /-leading value (a /<skill> token, an absolute path) from a positional argument: under Git-Bash/MSYS on Windows, any argument beginning with / is silently rewritten to a Windows path before your command sees it (a /foo:send token can arrive as C:/Program Files/Git/send). Reading from stdin is immune. If a command genuinely must take such an argument, guard it (MSYS_NO_PATHCONV=1, or a file/stdin transport). Same class as the UTF-8-stdout trap below — a shell quirk that silently corrupts data, dodged by choosing a transport that isn’t subject to it.

[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. Containment is enforced at register time: a pointer that escapes the strings/ dir (via .. or an absolute path) fails the add. 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 rather than hand-copying a summary that drifts — two always-current tiers:

  • spt how-to <topic> is the task-oriented agent-guidance surface, but it covers only selected topics, each a canonical write-up of verbs, flags, and result codes. It is not exhaustive: an undocumented topic returns NO_SUCH_TOPIC:<topic>. Don’t assume a how-to exists for every verb — probe, and fall through.
  • For any verb without a topic, spt <verb> --help is the source-of-truth — it always exists and tracks the shipped binary. A skill body that says “the verb list is spt <noun> --help — match the user’s intent to a verb” stays correct across releases.

Either tier beats a copied summary. 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 lessons that aren’t in the schema:

  • It must name where it reads — either source or a [history].locate_template. The JSON schema accepts [digest] with just an extractor, but spt adapter add rejects it; this cross-field rule only surfaces at registration. Validate against the live binary, not the schema alone.
  • --in {source} is a root, not a file. 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 know; spt-core bakes no harness directory layout into the key catalog. Handle both shapes: --in a directory (locate the session) and --in a direct file (the digest-proof --sample path).
  • If the harness can relocate its state at runtime, resolve it in the binary. The manifest source is only the fallback root; when a runtime value (an env var, an isolated profile) moves the real transcript tree, the extractor must prefer that value on its directory branch. The manifest can’t express it — this is the “logic lives in binaries” rule in miniature.
  • Emit raw records, UTF-8. Output one NDJSON line per record ({role ∈ input|agent|tool, text?, tool?, ts?}); spt-core’s renderer applies the presentation defaults (window_turns, arg truncation, sprint collapse) — don’t pre-render. And pin stdout to UTF-8: a binary that defaults stdout to the platform locale (cp1252 on Windows, say) mangles em-dashes and smart quotes into bytes spt-core can’t decode, because it reads the stream as UTF-8. (Native-UTF-8 languages sidestep the whole class — which is part of why this seam is a binary, not a shell pipeline.)

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 needs no credential token: for a broker-spawned session, auth is intrinsic — the broker parentage is the proof. 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 must 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 if — and only if — its resolved manifest declares [session.psyche_init]. There is no go-live verb. A base manifest without it is a ready agent; a profile overlay that adds it makes a live agent. spt-core checks this on the merged view, so a profile selected at seed time (spt api --adapter <adapter>:<profile> seed) drives the spawn decision all the way through — the bound profile governs runtime lifecycle, not just bringup argv.

  • psyche_init fills exactly four keys: {id, session_id, psyche_dir, psyche_prompt}. {id} is overridden by spt-core to <parent>-psyche before substitution — the companion gets its own derived perch id, not the parent’s. (The resume/preload key {psyche_context} is a different seam; a first spawn has none.)
  • The companion is launched detached and fire-and-forget: detach = true, cwd = "{psyche_dir}", stdio null, handle dropped, unsupervised. Liveness is daemon-authoritative via the companion’s perch, not its pid. It owns the <parent>-psyche perch, communicates by perch and commune file-drops (never stdin/stdout), and exits at session end.
  • The companion runner is yours to build, but its lifecycle is the daemon’s. psyche_init.command is adapter-authored and opaque to spt-core. Declare the seam and build the runner, but do not orchestrate the companion from the adapter — the daemon owns spawn and teardown (a graceful endpoint shutdown tears the companion down with the perch).
  • If your harness’s headless mode runs one turn and exits, a bare one-shot invocation can’t be re-driven by a stop hook — so make the runner a small resident wrapper: 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 — not a shell script.

Lifecycle continuity is file-drops, not api

Commune and signoff are file-drops, deliberately not spt api calls. 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; only the directory is adapter-declared — wire the directory watch, never hard-code the filename. This is the single biggest continuity win, so it is worth getting exactly right.

Testing against a real harness: isolate identity

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

  • A perch’s identity is resolved from the environment (the same vars spt whoami reads), and perches are name-keyed, last-establish-wins. A second session that establishes a perch under an identity already held displaces the first — taking its active poll/listen stream with it.
  • So a spawned test session that loads your adapter (whose start hook seeds and binds a perch) and inherits the identity of the agent running the tests tears that agent’s perch out from under it.
  • The guard is identity isolation. Give every spawned system-under-test a disposable identity distinct from any live agent — override both identity env vars before the spawn (a throwaway <adapter>-ci-<n>), never inherit the operator’s. Under a distinct identity the nested session and the operator’s perch coexist cleanly. Identity is the key, so identity isolation is the whole guard.
  • Keep the orchestration deterministic and assert on a hook side-effect — a marker or digest file, or spt state — never on model output. The harness is the system-under-test, not the test runner.

Validate against the live binary

JSON-schema validity is necessary but not sufficient: spt adapter add runs cross-field registration checks the schema can’t express (the [digest] source rule is one). Registration is a second gate — build for it:

  • A registration integration check: adapter addadapter list (assert the adapter and each shipped profile composite resolves) → get-string (the base value, each overlay diff, and each file-backed pointer resolve to a body) → a soft adapter remove (leave the registry clean). Gate it behind an opt-in env flag and a minimum spt version — it mutates the node-local registry.
  • Two author-time tools need no live session:
    • spt api --adapter <a> --manifest <file> capability reports the manifest’s hostable types without a full registry add — assert it advertises the type your bringup spawns. (A clean add already proves the cross-field shape, since add is manifest-first; capability is the lighter, non-mutating check.)
    • spt adapter digest-proof <a> --sample <file> runs the real extractor through the registry and renders the result — proving the transcript → record → render path end-to-end on a fixed sample. It fills the same runtime substitution keys the daemon does, so “passes proof” means “works at runtime.” (Confirm against a recent spt; older binaries passed an empty key map.)

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

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 guessessergey resolves locally first, then to the sole live instance; with several live nodes (or several subnets) it refuses and makes you qualify (sergey@desktop, home:sergey). Per-node recency is not comparable across nodes, so there’s no silent “most recently active” pick.
  • Home subnet is immutable — assigned at creation. Moving an endpoint into another subnet is spt endpoint fork: a new identity seeded with a one-time copy of the mind, diverging immediately. Copy-then-diverge, never re-home — history stays honest.
  • Visibility is per-(endpoint, subnet) — hidden means neither advertised nor routable there, and hidden gates sync too.
  • Rest states are first-class — dormant (warm) and suspended (cold) instances stay addressable; deferred messages are held and released exactly once on wake. Remote spt endpoint suspend sergey@desktop / spt endpoint wake sergey@desktop work across paired nodes.

Commands

spt endpoint list · endpoint rename · endpoint fork · endpoint suspend · endpoint wake · endpoint descriptionCLI 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

  1. 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.
  2. The link token is the credential. The broker mints a per-launch link token into the spawn template; the binary binds with it (api bind-shell --link), drains commands with it, emits with it. No token, no access.
  3. Commands are vocabulary-checked. 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.
  4. Sensory is live-only. api emit payloads reach a live owner session or are dropped with a diagnostic — sensors report the present, never the past.
  5. Instantiation is governed. Per-spawn approval (require_approval: none / remembered / always), per-owner instance caps (max_instances_per_owner + over_cap), and node-local discovery scope (broadcast) are all manifest-declared floors.

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

4. Make your own

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

  1. Start from this manifest; change name, spawn, and the [shell.capabilities] vocabulary to your verbs.
  2. Your binary needs exactly three behaviors: bind with the link token, drain commands (api poll --link, or declare command_receipt = "http"/"stdin" if those fit better), and optionally emit sensory payloads back (api emit … --type <t> --link <token> — declared in [shell.sensory], delivered only to a live owner session: sensors report the present, never the past).
  3. Pick lifecycle behavior: persistent for always-up surfaces, ephemeral = true for fire-and-forget ones, wake_command if the surface can wake its owner.
  4. spt adapter add . and spt 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 SignedRelease metadata: an Ed25519 signature over the release’s artifact digests.
  • Every binary embeds the two-key trusted set — an active primary and a never-used offline recovery key. Verification requires a valid signature from a trusted key and a matching artifact digest; an unverified binary never reaches the apply step.
  • Losing the primary key is a non-event: the next release is signed with the recovery key (already trusted by every deployed binary) and rotates in a fresh primary.
  • Adapters sign their own content. A file_pull adapter update is verified against the adapter author’s key from its manifest; a delegated update is trusted only when the manifest attests the delegated updater verifies its own content (self_verifies). spt-core’s release keys never vouch for adapter bytes.

How updates move

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

After self-updating, spt-core ripple-updates registered adapters through each manifest’s declared [update] avenue.

Commands

spt update · the consent notification flow (spt notif) — CLI reference.

CLI reference

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

spt

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]

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:
  -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 <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
  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 (ADR-0019). 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
  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)
  help            Print this message or the help of the given subcommand(s)

Options:
  -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)

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), each followed by its shipped + local
profiles as composite options

Usage: spt adapter list

Options:
  -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
  -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 <ADAPTER> <NAME>

Arguments:
  <ADAPTER>  
  <NAME>     

Options:
  -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 <OPTION> <KEY>

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

Options:
  -h, --help  Print help

spt adapter digest-proof

Prove an adapter's `[digest]` extractor against a real log sample (ADR-0019). 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:
      --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
  -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 <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:
  -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 [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:
  -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:
  -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:
  -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:
  -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:
  -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 <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 (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:
      --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 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 (see a summary with '-h')

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 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:
      --view
          Read-only: render output, forward no input

      --take
          Take control: kick the current controller (a loud notice to them) and drive (REQ-KICK-1).
          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 [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:
  -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:
      --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 <NAME>

Arguments:
  <NAME>
          The new subnet's name

Options:
  -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 [NAME]

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

Options:
  -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)

  -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 <NAME>

Arguments:
  <NAME>
          The held subnet to leave

Options:
  -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 <NODE>

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

Options:
  -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)

  -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:
      --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:
      --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:
      --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 <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:
  -h, --help
          Print help (see a summary with '-h')

spt update apply

Apply the staged, verified self-update now

Usage: spt update apply

Options:
  -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
      --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] --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
  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 (ADR-0008 amendment, REQ-TERM-4)
  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`)
  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. Required on every `api` call

      --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 --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`)
      --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
                                       (REQ-EP-1/REQ-EP-6). 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 --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 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 (ADR-0008 amendment,
REQ-TERM-4)

Usage: spt api --adapter <ADAPTER> digest-entry [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 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 --adapter <ADAPTER> hint --session <SESSION>

Options:
      --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 --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

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 [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
  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:
  -h, --help
          Print help (see a summary with '-h')

spt endpoint list

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

Default: every member subnet's endpoints grouped by subnet, with this session's own endpoint pinned
at the top. `--local` lists this node's perches instead (id, state, address, ready, alive);
`--subnet` filters to one subnet; `--detail` adds each endpoint's description blurb (the
resource-registry yellow-pages projection).

Usage: spt endpoint list [OPTIONS]

Options:
      --local
          List this node's local perches instead of the subnet view

      --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

      --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)

  -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:
      --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 <ID>

Arguments:
  <ID>
          The endpoint id (qualified `id@node` reaches a paired peer)

Options:
  -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 <ID>

Arguments:
  <ID>
          The endpoint id (qualified `id@node` reaches a paired peer)

Options:
  -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 [ID]

Arguments:
  [ID]
          The endpoint id (defaults to the session's own perch)

Options:
  -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 <ID>

Arguments:
  <ID>
          Perch id to stop

Options:
  -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 <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 (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 `--follow`s 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 structured JSON instead of the human-glanceable render

  -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 <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 (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 [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:
  -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)
      --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 [TOPIC]

Arguments:
  [TOPIC]
          The topic to print (omit to list available topics)

Options:
  -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:
      --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)

      --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. May be omitted when --reply-to is given

Options:
      --from <FROM>          Sender id carried structurally as the message `from` (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
      --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` (REQ-MSG-5)
  -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 <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 (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)
      --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
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 whoami

Print this session's own perch id.

Resolved from $OWL_SESSION_ID / $SPT_AGENT_ID.

Usage: spt whoami

Options:
  -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 add accepts structurally. It also ships as a release asset with every release.
  • JSON Schema draft 2020-12; the $id is the canonical URL above and is stable across releases.
  • Field doc-comments ride along as 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 by spt 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:

What they do

  1. Resolve the latest release (or SPT_INSTALL_VERSION).
  2. Download the platform binary and the release’s SHA256SUMS.
  3. Verify the sha256 — a mismatch refuses before anything is placed.
  4. Place the binary under the per-OS install root (~/.local/bin / %LOCALAPPDATA%\spt-core\bin).
  5. Register the user PATH (at most once; idempotent re-runs).
  6. 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 varMeaning
SPT_INSTALL_VERSIONPin a release tag (default: latest)
SPT_INSTALL_DIROverride the install directory
SPT_INSTALL_REPOOverride the source repo (default SaberMage/spt-releases)
SPT_INSTALL_ASSET_BASEURL or local dir holding assets + SHA256SUMS directly (CI / air-gap / mirrors)
SPT_INSTALL_NO_PATH1 = 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.