--- meta-description: spt-core: a harness-independent core for an agent ecosystem — messaging, live-agent lifecycle, terminal hosting, P2P networking, and the runtime-manifest harness contract. meta-theme-color: #ffffff meta-viewport: width=device-width, initial-scale=1 title: Integration checklist - SPT developer docs --- ## Keyboard shortcuts Press `←` or `→` to navigate between chapters Press `S` or `/` to search in the book Press `?` to show this help Press `Esc` to hide this help ![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48cGF0aCBkPSJNMCA5NkMwIDc4LjMgMTQuMyA2NCAzMiA2NEg0MTZjMTcuNyAwIDMyIDE0LjMgMzIgMzJzLTE0LjMgMzItMzIgMzJIMzJDMTQuMyAxMjggMCAxMTMuNyAwIDk2ek0wIDI1NmMwLTE3LjcgMTQuMy0zMiAzMi0zMkg0MTZjMTcuNyAwIDMyIDE0LjMgMzIgMzJzLTE0LjMgMzItMzIgMzJIMzJjLTE3LjcgMC0zMi0xNC4zLTMyLTMyek00NDggNDE2YzAgMTcuNy0xNC4zIDMyLTMyIDMySDMyYy0xNy43IDAtMzItMTQuMy0zMi0zMnMxNC4zLTMyIDMyLTMySDQxNmMxNy43IDAgMzIgMTQuMyAzMiAzMnoiIC8+PC9zdmc+) ![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIj48cGF0aCBkPSJNMzcxLjMgMzY3LjFjMjcuMy0zLjkgNTEuOS0xOS40IDY3LjItNDIuOUw2MDAuMiA3NC4xYzEyLjYtMTkuNSA5LjQtNDUuMy03LjYtNjEuMlM1NDkuNy00LjQgNTMxLjEgOS42TDI5NC40IDE4Ny4yYy0yNCAxOC0zOC4yIDQ2LjEtMzguNCA3Ni4xTDM3MS4zIDM2Ny4xem0tMTkuNiAyNS40bC0xMTYtMTA0LjRDMTc1LjkgMjkwLjMgMTI4IDMzOS42IDEyOCA0MDBjMCAzLjkgLjIgNy44IC42IDExLjZjMS44IDE3LjUtMTAuMiAzNi40LTI3LjggMzYuNEg5NmMtMTcuNyAwLTMyIDE0LjMtMzIgMzJzMTQuMyAzMiAzMiAzMkgyNDBjNjEuOSAwIDExMi01MC4xIDExMi0xMTJjMC0yLjUtLjEtNS0uMi03LjV6IiAvPjwvc3ZnPg==) - Auto - Light - Rust - Coal - Navy - Ayu ![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNNDE2IDIwOGMwIDQ1LjktMTQuOSA4OC4zLTQwIDEyMi43TDUwMi42IDQ1Ny40YzEyLjUgMTIuNSAxMi41IDMyLjggMCA0NS4zcy0zMi44IDEyLjUtNDUuMyAwTDMzMC43IDM3NmMtMzQuNCAyNS4yLTc2LjggNDAtMTIyLjcgNDBDOTMuMSA0MTYgMCAzMjIuOSAwIDIwOFM5My4xIDAgMjA4IDBTNDE2IDkzLjEgNDE2IDIwOHpNMjA4IDM1MmM3OS41IDAgMTQ0LTY0LjUgMTQ0LTE0NHMtNjQuNS0xNDQtMTQ0LTE0NFM2NCAxMjguNSA2NCAyMDhzNjQuNSAxNDQgMTQ0IDE0NHoiIC8+PC9zdmc+) # SPT developer docs [![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMTI4IDBDOTIuNyAwIDY0IDI4LjcgNjQgNjR2OTZoNjRWNjRIMzU0LjdMMzg0IDkzLjNWMTYwaDY0VjkzLjNjMC0xNy02LjctMzMuMy0xOC43LTQ1LjNMNDAwIDE4LjdDMzg4IDYuNyAzNzEuNyAwIDM1NC43IDBIMTI4ek0zODQgMzUydjMyIDY0SDEyOFYzODQgMzY4IDM1MkgzODR6bTY0IDMyaDMyYzE3LjcgMCAzMi0xNC4zIDMyLTMyVjI1NmMwLTM1LjMtMjguNy02NC02NC02NEg2NGMtMzUuMyAwLTY0IDI4LjctNjQgNjR2OTZjMCAxNy43IDE0LjMgMzIgMzIgMzJINjR2NjRjMCAzNS4zIDI4LjcgNjQgNjQgNjRIMzg0YzM1LjMgMCA2NC0yOC43IDY0LTY0VjM4NHptLTE2LTg4Yy0xMy4zIDAtMjQtMTAuNy0yNC0yNHMxMC43LTI0IDI0LTI0czI0IDEwLjcgMjQgMjRzLTEwLjcgMjQtMjQgMjR6IiAvPjwvc3ZnPg==)](../print.html "Print this book") [![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OTYgNTEyIj48cGF0aCBkPSJNMTY1LjkgMzk3LjRjMCAyLTIuMyAzLjYtNS4yIDMuNi0zLjMuMy01LjYtMS4zLTUuNi0zLjYgMC0yIDIuMy0zLjYgNS4yLTMuNiAzLS4zIDUuNiAxLjMgNS42IDMuNnptLTMxLjEtNC41Yy0uNyAyIDEuMyA0LjMgNC4zIDQuOSAyLjYgMSA1LjYgMCA2LjItMnMtMS4zLTQuMy00LjMtNS4yYy0yLjYtLjctNS41LjMtNi4yIDIuM3ptNDQuMi0xLjdjLTIuOS43LTQuOSAyLjYtNC42IDQuOS4zIDIgMi45IDMuMyA1LjkgMi42IDIuOS0uNyA0LjktMi42IDQuNi00LjYtLjMtMS45LTMtMy4yLTUuOS0yLjl6TTI0NC44IDhDMTA2LjEgOCAwIDExMy4zIDAgMjUyYzAgMTEwLjkgNjkuOCAyMDUuOCAxNjkuNSAyMzkuMiAxMi44IDIuMyAxNy4zLTUuNiAxNy4zLTEyLjEgMC02LjItLjMtNDAuNC0uMy02MS40IDAgMC03MCAxNS04NC43LTI5LjggMCAwLTExLjQtMjkuMS0yNy44LTM2LjYgMCAwLTIyLjktMTUuNyAxLjYtMTUuNCAwIDAgMjQuOSAyIDM4LjYgMjUuOCAyMS45IDM4LjYgNTguNiAyNy41IDcyLjkgMjAuOSAyLjMtMTYgOC44LTI3LjEgMTYtMzMuNy01NS45LTYuMi0xMTIuMy0xNC4zLTExMi4zLTExMC41IDAtMjcuNSA3LjYtNDEuMyAyMy42LTU4LjktMi42LTYuNS0xMS4xLTMzLjMgMi42LTY3LjkgMjAuOS02LjUgNjkgMjcgNjkgMjcgMjAtNS42IDQxLjUtOC41IDYyLjgtOC41czQyLjggMi45IDYyLjggOC41YzAgMCA0OC4xLTMzLjYgNjktMjcgMTMuNyAzNC43IDUuMiA2MS40IDIuNiA2Ny45IDE2IDE3LjcgMjUuOCAzMS41IDI1LjggNTguOSAwIDk2LjUtNTguOSAxMDQuMi0xMTQuOCAxMTAuNSA5LjIgNy45IDE3IDIyLjkgMTcgNDYuNCAwIDMzLjctLjMgNzUuNC0uMyA4My42IDAgNi41IDQuNiAxNC40IDE3LjMgMTIuMUM0MjguMiA0NTcuOCA0OTYgMzYyLjkgNDk2IDI1MiA0OTYgMTEzLjMgMzgzLjUgOCAyNDQuOCA4ek05Ny4yIDM1Mi45Yy0xLjMgMS0xIDMuMy43IDUuMiAxLjYgMS42IDMuOSAyLjMgNS4yIDEgMS4zLTEgMS0zLjMtLjctNS4yLTEuNi0xLjYtMy45LTIuMy01LjItMXptLTEwLjgtOC4xYy0uNyAxLjMuMyAyLjkgMi4zIDMuOSAxLjYgMSAzLjYuNyA0LjMtLjcuNy0xLjMtLjMtMi45LTIuMy0zLjktMi0uNi0zLjYtLjMtNC4zLjd6bTMyLjQgMzUuNmMtMS42IDEuMy0xIDQuMyAxLjMgNi4yIDIuMyAyLjMgNS4yIDIuNiA2LjUgMSAxLjMtMS4zLjctNC4zLTEuMy02LjItMi4yLTIuMy01LjItMi42LTYuNS0xem0tMTEuNC0xNC43Yy0xLjYgMS0xLjYgMy42IDAgNS45IDEuNiAyLjMgNC4zIDMuMyA1LjYgMi4zIDEuNi0xLjMgMS42LTMuOSAwLTYuMi0xLjQtMi4zLTQtMy4zLTUuNi0yeiIgLz48L3N2Zz4=)](https://github.com/SaberMage/spt-releases "Git repository") ![SVG Image](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBkPSJNMzA0IDQ4YzAtMjYuNS0yMS41LTQ4LTQ4LTQ4cy00OCAyMS41LTQ4IDQ4czIxLjUgNDggNDggNDhzNDgtMjEuNSA0OC00OHptMCA0MTZjMC0yNi41LTIxLjUtNDgtNDgtNDhzLTQ4IDIxLjUtNDggNDhzMjEuNSA0OCA0OCA0OHM0OC0yMS41IDQ4LTQ4ek00OCAzMDRjMjYuNSAwIDQ4LTIxLjUgNDgtNDhzLTIxLjUtNDgtNDgtNDhzLTQ4IDIxLjUtNDggNDhzMjEuNSA0OCA0OCA0OHptNDY0LTQ4YzAtMjYuNS0yMS41LTQ4LTQ4LTQ4cy00OCAyMS41LTQ4IDQ4czIxLjUgNDggNDggNDhzNDgtMjEuNSA0OC00OHpNMTQyLjkgNDM3YzE4LjctMTguNyAxOC43LTQ5LjEgMC02Ny45cy00OS4xLTE4LjctNjcuOSAwcy0xOC43IDQ5LjEgMCA2Ny45czQ5LjEgMTguNyA2Ny45IDB6bTAtMjk0LjJjMTguNy0xOC43IDE4LjctNDkuMSAwLTY3LjlTOTMuNyA1Ni4yIDc1IDc1cy0xOC43IDQ5LjEgMCA2Ny45czQ5LjEgMTguNyA2Ny45IDB6TTM2OS4xIDQzN2MxOC43IDE4LjcgNDkuMSAxOC43IDY3LjkgMHMxOC43LTQ5LjEgMC02Ny45cy00OS4xLTE4LjctNjcuOSAwcy0xOC43IDQ5LjEgMCA2Ny45eiIgLz48L3N2Zz4=) # [Harness integration checklist](#harness-integration-checklist) A working list for building a harness against spt-core. The [adapter quickstart](../quickstart/adapter.html) gets one adapter breathing in ten minutes; this page is the *complete* surface — every manifest section and `spt api` command a harness touches, **grouped by how badly you need it**, each tagged with the **feature it buys** and **where in the interaction lifecycle** it fires. Two seams only (the [contract overview](overview.html)): the [manifest](manifest.html) (declarative TOML) and the [`spt api` surface](api.html) (imperative entry points your hooks fire). Nothing here is an SDK call — everything is a manifest field or an `spt` invocation. > **The running example is [spt-claude-code](https://github.com/SaberMage/spt-releases)** — > the modern Claude Code harness rebuilt on spt-core (the v1 reference adapter). > Where a row says *“claude-code: …”* that is how that harness wires the > surface. Concrete commands below are real and shippable today; the shipped > harness-agnostic exercise is the [mock adapter](../quickstart/adapter.html). ## [The interaction lifecycle](#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)](#group-1--required-no-adapter-exists-without-these) The contract floor. Miss one and spt-core cannot host your sessions. | Surface | Feature it buys | Lifecycle stage | | --- | --- | --- | | **`[adapter]` manifest header** (`name`, `kind`, `version`, `min_spt_core_version`, `hostable_types`) | Identity + the compat gate spt-core reads *before* any install/update; declares which endpoint types you can host | REGISTER | | **`spt adapter add `** | Parses + schema-validates + records the manifest; a bad field is rejected here, nothing half-registers | REGISTER | | **`[adapter] host_binaries`** (harness-hosted) | The bind-time match-key — names the harness exe(s) you host, so `seed`/`listen` resolve your adapter with **no `--adapter`** (since v0.9.0). `--adapter ` stays available as an optional override | REGISTER | | **Startup pair — pick one flow:** • harness-hosted: `[hooks.SessionStart] → api seed --pid {parent_pid} --session-id {session_id}` then the session’s `api listen ` • spt-hosted: `[session.self]` template (spt-core spawns it) then `api bind --set-session-id ` | A registered, held perch — the thing messages and lifecycle attach to. `seed→listen` = you own the process; `spawn→bind` = spt-core owns it | START | | **`api session-end `** (or `api shutdown`, below) | Clean teardown that PRESERVES the spool + history so the next `listen`/`poll` drains the backlog | END | **claude-code:** `SessionStart` hook fires `api seed`; the Claude Code session runs `api listen` as its blocking listener (harness-hosted). `SessionEnd` fires `api session-end` (soft — context survives a `/clear` and a relaunch). --- ## [Group 2 — Recommended (the integration is hollow without them)](#group-2--recommended-the-integration-is-hollow-without-them) Skippable to *boot*, but the harness feels broken without them — no inbound messages, identity lost on a context reset, no activity signal. | Surface | Feature it buys | Lifecycle stage | | --- | --- | --- | | **`[hooks.Idle] → api state idle`** (and `api state busy`) | Honest activity — spt-core never infers idleness from terminal quiescence (it lies). Arms the echo gate, drives Psyche pulses + most-recently-active routing | RUN | | **`[inject]` channels** (`activity` / `idle`) + **`api poll --include-deferred`** | Inbound message delivery. Declares HOW spt-core reaches the agent (hook inject vs. pull-relay); `poll` is the pull path for hooks that can’t inject | RUN | | **Honest `can_inject` per hook** | Lets spt-core route around a hook that can’t surface text — the load-bearing harness-varying fact | RUN | | **`api boundary --to-session-id --session-id `** | The endpoint’s identity, spool, and history survive a context reset under a new session id. **The proof is the PRIOR sid** (`--to-session-id` is payload, not proof): persist the current sid at every SessionStart in adapter-owned state keyed by endpoint id, present it here; resolve the id from `$SPT_ENDPOINT_ID`; surface any refusal LOUDLY — a silent skip/refusal strands the perch on the dead sid (no delivery until relaunch). **Validate end-to-end: after a reset, assert the perch record’s session id actually ROTATED and a post-reset message DELIVERS** — a session that looks healthy can hide a stranded perch (see `api boundary` in api.md) | BOUNDARY | | **`api psyche-download `** (fire from SessionStart, inject its stdout) | The agent resumes **with its mind** — pulls the durable two-tier context (role / live / project) **plus** any not-yet-synthesized commune as `` slices, for the hook to inject as additional context. `api boundary` makes the mind *survive* a reset; this is how the next session reads it **back in**. Without it, a resumed session starts blank of its accumulated context | BOUNDARY / START | | **`[history]` strategy** (`fetcher` / `locate_normalize` / `native` + `api history-log`) | spt-core can read the session transcript — feeds the live digest and mind sync | RUN | | **`[identity]`** (`session_id_source`, `parent_ancestor_name`) | Post-spawn id resolution when the harness mints the session id itself | START | | **`[env.*]` bridge** (e.g. `OWL_SESSION_ID`) | The session learns its own endpoint id / context the harness must inject | START | | **`[update]` avenue + command** | Ripple-update: spt-core refreshes your adapter alongside its own self-update (REQ-UPD-5); also the install-on-demand bootstrap | KEEP-CURRENT | | **`[update.post]` post-step** (since v0.16.0) | A delegated step that runs **after** the primary avenue resolves, under the same `spt adapter update` — pull the `.spt` **and** run an in-harness sync from one lever. Runs unconditionally; reads a published JSON line on stdin (`adapter_applied`, `version`, `previous_version`, `adapter_dir`, …); its stdout decides the post-update notice (custom text supersedes `[update].message`, the sentinel `!!update-message!!` fires it, empty is silent); failure-isolated (warn + fall back, never rolls back the pull) | KEEP-CURRENT | **claude-code:** `Idle` hook → `api state idle`; messages arrive over the hook inject channel (`can_inject = true`), pull-relay fallback when busy. `PreCompact`/clear hooks → `api boundary`; the `SessionStart` hook also runs `api psyche-download ` and injects stdout, so a session resumes with its accumulated two-tier mind (plus any pending commune). `[history] strategy = "fetcher"` (Claude Code’s transcript is a binary the fetcher reads). `[update] avenue = "delegated"`, `command = "claude plugin update spt"` — the harness’s own updater is the avenue. --- ## [Group 3 — Optional (capability-specific)](#group-3--optional-capability-specific) Reach for these when the capability applies; ignore them otherwise. | Surface | Feature it buys | Lifecycle stage | | --- | --- | --- | | **`api shutdown `** | Graceful signoff — runs the final echo-commune BEFORE teardown so the context delta is never lost to ordering | END | | **`api presence ` / `api driven-by `** | Most-recently-active resolution across the subnet; lets a session tell local input from remote-drive | RUN | | **Workers** (`api worker-start ` — the worker id is core-minted `-w`, read it from stdout; `worker-poll `/`worker-stop ` auth by the parent’s session id, no token — breaking change in v0.27.0) | Nested, short-lived sub-agents under a parent endpoint | RUN | | **`[digest]` extractor** (or `api digest-entry`) | A live activity digest (`spt endpoint digest`) — declare an extractor mapping your native log → the `{role, text, tool, ts}` contract (ADR-0019; its OWN seam, no longer riding `[history]`). Spans `/clear` via the session ledger; validate with `spt adapter digest-proof`. **Classify a delivered user-facing message as a turn-opening `input` record** (see below) so the v0.16.0 `--last`/`seq` cursor keeps its granularity | RUN | | **`[session.notif]` template** | Native OS notification render (toast / shell alert) for consent + capability prompts, instead of burying them in agent output | RUN | | **`[session.resume]` template** (spt-hosted) | The **native-resume** sibling of `[session.self]`: spt-core picks it over `[session.self]` when a bringup carries a prior session (`spt endpoint run --resume`, or the picker’s *Resume from history*). Declare your harness’s native-resume verb (e.g. `claude -r {session_id}`) — **skip it and a resume re-runs the fresh command → a blank transcript.** spt-core lands the PTY in the session’s recorded project cwd (a harness resolves a transcript by `session_id` + cwd) | START | | **`[message-idle-translation-binary]`** (spt-hosted) | A lifecycle-managed `stdin→stdout` JSON-lines binary that turns inbound `` messages into keystroke-commands spt-core applies to the PTY **atomically** (coexists with a live `spt rc` controller). The agnostic way to deliver messages into an **idle** spt-hosted session; busy delivery stays your `[inject]` hook path. Declare it with a `command` (program + args; adapter-static `{adapter_dir}`/`{adapter_name}` subst only — **no** session keys; new in v0.16.0, the bare `path` is deprecated). Validate the emit contract with `spt adapter translate-proof` | RUN | | **`[adapter] shortcut_basename`** | Names the picker-generated project-root launcher `-` (the `spt endpoint run` `s` keybind) — your harness’s brand instead of the `spt-` default | START | | **Shell surfaces** (`kind = "shell"`: `api bind-shell --link`, `api emit`, `api owner-shutdown`, the `[shell]` body) | Driven surfaces — notifiers, sensors, power buttons — authenticated by the launch link token alone. See [Shells](../shells/overview.html) | START / RUN | **claude-code:** uses `api shutdown` for graceful `/signoff`; declares a `[digest]` extractor mapping its per-session JSONL → the digest-record contract so `spt endpoint digest` shows live tool calls and spans `/clear`; declares `shortcut_basename = "cc"` so the picker’s generated launcher is `cc-` (vs the `spt-` default); declares `[session.resume]` as `claude -r {session_id} …` so a picker *Resume from history* reloads the real transcript (not a blank session); declares a `[message-idle-translation-binary]` (`cc-spt-idle-translate`) so inbound messages reach an idle session as proper keystrokes; no shell body (it is a harness, not a driven surface). --- ## [Group 4 — Beyond the API: integrations that make it good](#group-4--beyond-the-api-integrations-that-make-it-good) Not contract surfaces — no `api` command, no required field — but the difference between an adapter that *works* and one that feels native. **Strongly recommended.** | Integration | What it is | Why it matters | | --- | --- | --- | | **Commune / signoff file-drops** | The agent writes `-commune.md` (delta context) or `-signoff.md` (final save) into the manifest’s watched `commune_dir` / `signoff_dir`; spt-core’s watcher ingests it. **Delivered as a file-drop by design.** | The two-tier mind: live + project context survives `/clear`, `/compact`, suspend, and cross-node resume. The single biggest continuity win — wire the directory watch and read the contract filename | | **Resource advertisement** (`[session] resources` blurb / `spt endpoint description`) | A free-text “what I can serve” string riding the endpoint’s registry rows | Other agents discover the endpoint’s capabilities (`spt resources list`) instead of guessing | | **Install-on-demand bootstrap** | Pack the check-and-install of spt-core into your harness’s first run (the [bootstrap pattern](install-on-demand.html)) | Zero-friction first run — the user installs your harness, spt-core comes with it | | **Surfacing `spt how-to ` to the agent** | Let the agent read task-oriented spt-core guidance from the binary itself | The agent self-serves common operations (subnet join, sending) instead of asking the user | | **Presence-driven idle reporting** | Fire `api state idle` from a *real* user-inactivity signal, not a timer | Accurate dormancy → Psyche wakes on genuine activity, echo-communes fire at true boundaries | **claude-code (the worked example):** ships the modern two-tier mind end to end — the session drops `-commune.md` at every `/clear` and `/compact`, and a Self-authored `-signoff.md` at graceful stop, into the watched `commune_dir`; declaring `[session.psyche_init]` promotes the endpoint to a LiveAgent (a **go-live gate** — spt-core never spawns it), and the `[session.psyche_resume]` per-event turn (+ the `[session.echo_commune]` template) lets spt-core drive the Psyche that ingests them; `[update] avenue = "delegated"` makes the Claude Code plugin updater the ripple avenue. That is the bar a native-feeling harness clears. --- ## [Patterns introduced in v0.16.0](#patterns-introduced-in-v0160) ### [Hook dispatch by resolve-not-execute](#hook-dispatch-by-resolve-not-execute) spt-core never grows a hook-**execution** surface — `[hooks.]` stays purely outbound (the harness fires `fires`; spt-core never runs a hook handler). When your hook *logic* must live in an adapter binary (so it rides `spt adapter update`) but the harness loads hooks from a static plugin dir, use the two adapter-static substitution keys to resolve+run **your own** binary: - **`{adapter_dir}`** fills to your install dir (the registry `source_dir`) and **survives updates**; **`{adapter_name}`** fills to your adapter name. Both are available wherever substitution runs — including, new in v0.16.0, **inside `[strings]` values at `get-string` read time** (scoped to *just* these two adapter-static keys; `get-string` has no session context, so `{id}`/ `{session_id}` are not available there). - Store the dispatch command in `[strings]`: [strings] hook_cmd = "{adapter_dir}/claude-spt hook" - A thin, static per-OS dispatch wrapper (the one plugin-resident piece) runs `spt adapter get-string hook_cmd` **once per session** (memoize the resolved string into an env var for a hot-path hook like PostToolUse), then executes the resolved command per-hook itself. spt-core only **resolves and returns** the string — it never executes it (ADR-0029). **claude-code:** the plugin ships a static `hooks.json` + a per-OS dispatch wrapper; the wrapper resolves `get-string claude-spt hook_cmd` → `/claude-spt hook` once per session and runs it per-hook, so all hook logic updates via `spt adapter update claude-spt`. ### [Incremental digest consumption — the `--json` cursor](#incremental-digest-consumption--the---json-cursor) `spt endpoint digest --json` supports turn-end incremental consumption (v0.16.0): `--last ` (the last N turns; `--last 1` = the latest turn), a stable per-entry **`seq`** (source-derived — re-projection yields the same `seq`; it does not renumber when the window slides), and `--after ` (entries newer than `seq` still in the window; a full-window refresh + a predates signal if `seq` has fallen out). The trailing in-progress turn is flagged `partial: true` and its entries carry no stable `seq` until the turn closes (a turn is bounded by a user-input) — a consumer reprocesses `partial` and skips entries `<= seq`. `seq` is the authoritative dedup + cursor key. #### [The `--json` output shape](#the---json-output-shape) The snapshot is one pretty-printed JSON object; `--follow --json` emits one **compact** JSON object per line (a delta stream). Shapes as of v0.26.0: { "turns": [ { "input": "fix the bug", "input_seq": 4294967296, "entries": [ { "Agent": { "text": "on it", "seq": 4294967297, "ts": "2026-07-06T09:00:00Z" } }, { "ToolSprint": { "tools": [ { "name": "Write", "arg": "src/a.rs" } ], "seq": 4294967298 } }, { "Boundary": { "kind": "clear", "ts": "2026-07-06T09:05:00Z" } }, { "Context": { "kind": "owl_message", "body": "ping", "ts": null } } ] } ] } - **Turn**: `input` is the opening user-input text, `null` for a preamble turn (boundary/context entries that precede any input). `input_seq` and `partial` are **omitted** when absent/false — a trailing open turn carries `"partial": true` and its entries carry no `seq`. - **Entries are tagged by kind** — each entry is a one-key object whose key is the kind. The closed kind set: `Agent` (`text`, optional `seq`/`ts`), `ToolSprint` (`tools`: `{name, arg}` in order, `arg` presentation-truncated; optional `seq` = the **last** collapsed record’s, optional `ts`), `Boundary` (`kind`: `clear` | `compact` | `boot`), and `Context` (`kind`: `psyche_download` | `echo_mirror` | `owl_message`, plus `body`). `Agent`/`ToolSprint` omit `seq`/`ts` when absent; `Boundary`/`Context` carry no `seq` (they are spt-injected, not transcript records) and their `ts` is present-but-`null` when unknown. - **A delivered message can appear twice by design**: as the turn-opening `input` (your extractor’s classification, below) *and* as a `Context`/`owl_message` row whose `body` is the **whole composed `` envelope verbatim** — exactly what the agent saw. Parse it with the [envelope rules](../messaging/overview.html#the-event-wire-contract); it is the message-identity anchor for dedup across the two appearances. - **`--after` signal**: when the cursor predates the window, the snapshot is the full window plus a top-level `"after_predates_window": true`. - **stderr trailer**: every successful pull prints `DIGEST: version=` on stderr (the version pairs with the delta stream below); an endpoint with no activity buffer reports `NO_DIGEST:` and exits non-zero. - **`--follow --json` delta lines**: `{ "version": , "from": , "turns": [ … ] }` — apply by truncating your view to `from` turns and appending; `from == 0` is a full replace (the base snapshot, or a window slide). Deltas are sent only when the digest actually changed. **Binding for your `[digest]` extractor / `api digest-entry`:** classify a **delivered user-facing message as a turn-opening `input` record** (equivalent to a direct PTY user-input). The projection treats `role: "input"` as the turn boundary; if messaging-delivered turns are not opened as `input`, a messaging-driven session collapses into a few giant turns and `--last`/`seq` lose granularity. *What* becomes `input` is your call; *that it opens a turn* is the contract. ### [Global `--json` for read/status commands](#global---json-for-readstatus-commands) The read/status command set (`endpoint list`/`whoami`, `daemon status`, `subnet status`/`show-code`, `endpoint description`/`role`, `adapter list`/`version`, `notif list`, `grant list`, `access list`, `shell list`, `how-to`) honors a global **`--json`** flag (v0.16.0) for scripted consumption — stable, explicit per-command field names (a committed wire-parity surface). Action commands ignore it. Flag reference: the [CLI reference](../cli/reference.html). Committed compatibility posture for every `--json` shape: **additive evolution** — new fields appear (often omitted-when-absent), existing fields are never renamed or re-typed; parse tolerantly (ignore unknown keys). #### [`endpoint list --json` — the output shape](#endpoint-list---json--the-output-shape) One object, three sections (as of v0.27.0): { "self": { "id": "doyle", "status": "live_agent", "ready": true, "alive": true, "unbound": false, "description": null, "psyche_host_error": null }, "subnets": [ { "name": "home", "endpoints": [ { "id": "flynn", "node": "1a2b3c…", "node_label": "HFENDULEAM", "status": "Active", "resources": null, "endpoint_type": "live_agent", "project": "spt-mobile" } ] } ], "local": [ { "id": "doyle", "state": "live_agent", "address": "127.0.0.1:52110", "ready": true, "alive": true, "unbound": false, "project": "spt-core" } ] } - **`self`** — the calling session’s own endpoint, `null` when the session has no perch. `status` is the local perch state token (`live_agent`, `ready_agent`, …; `null` with no local perch), `ready`/`alive` likewise `null` for a pinless session. `description` is the endpoint’s authored description or `null`. Two fault annotations are **omitted entirely when absent**: `psyche_host_error` (string) and `translation_fault` (string) — presence means the human view shows the same fault line. - **`subnets`** — one group per subnet the node belongs to, `endpoints` from the subnet’s gossip projection. `status` is the ADVERTISED cross-node state, closed set: `Active` | `Dormant` | `Suspended` | `Offline`. `node` is the hosting node’s key prefix, `node_label` its display label (or `null`). `resources` is the endpoint’s advertised description string (or `null`). `endpoint_type` (`live_agent`, `ready_agent`, …) and `project` (latest project id) are **omitted when absent** — older rows may not carry them. - **`local`** — this node’s perches from the roster. `state` is the same token set as `self.status`; `address` is the listener address (or `null`); `project` omitted when absent. - **Filters apply before serialization**: worker endpoints are excluded from all three sections by default (v0.27.0) — pass `--workers` to include them; suspended rows honor `--all` the same way. `spt whoami --json` emits this same shape with the default filters. --- ## [“Am I done?” — the floor](#am-i-done--the-floor) - [ ] Manifest validates against [`manifest.schema.json`](https://sabermage.github.io/spt-releases/manifest.schema.json) - [ ] `[adapter]` header complete (`name`, `kind`, `version`, `min_spt_core_version`, `hostable_types`) - [ ] One startup flow wired: `SessionStart → seed` + `listen` (harness-hosted) **or** `[session.self]` + `bind` (spt-hosted) - [ ] (harness-hosted) `[adapter] host_binaries` names your harness exe(s) so `seed`/`listen` resolve with no `--adapter`; `spt adapter use ` sets the active default when several adapters host the same binary - [ ] `api state idle` fires on real inactivity; `can_inject` values are honest - [ ] An inbound delivery channel is declared (`[inject]`) or pulled (`api poll`) - [ ] `[history]` strategy chosen; `api boundary` wired for clear/compact - [ ] (mind continuity) SessionStart fires `api psyche-download` and injects its stdout, so a resumed session gets its durable context back - [ ] (for a live digest) `[digest]` extractor declared + `digest-proof`-checked, or `api digest-entry` push - [ ] (spt-hosted, if your harness resumes by id) `[session.resume]` declares the native-resume command — else a resume comes up blank - [ ] (spt-hosted, for idle message delivery) `[message-idle-translation-binary]` declared + `translate-proof`-checked, or accept the degenerate `payload+enter` inject - [ ] `[update]` avenue declared (ripple-update + install-on-demand) - [ ] Teardown fires `api session-end` (or `api shutdown` for graceful signoff) - [ ] **Recommended:** commune/signoff directory watched (mind continuity) - [ ] `spt adapter add ./your-adapter` registers clean; `api … capability` echoes your `hostable_types` ## [Next](#next) - **Reference:** the complete [manifest reference](manifest.html) and [`spt api` reference](api.html). - **Ship it:** the [install-on-demand bootstrap](install-on-demand.html). - **Driven surfaces:** [Shells](../shells/overview.html) — the `kind = "shell"` flavor of this same contract.