# Harness integration checklist

<!-- [doc->REQ-DOCS-2] the harness-author checklist: every contract surface a harness touches, grouped by necessity, mapped to the interaction lifecycle, with the modern Claude Code adapter (spt-claude-code) as the worked example -->

A working list for building a harness against spt-core. The
[adapter quickstart](../quickstart/adapter.md) 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.md)): the
[manifest](manifest.md) (declarative TOML) and the [`spt api` surface](api.md)
(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.md).

## The interaction lifecycle

Every surface below belongs to one stage of a harness's life with spt-core:

```text
 REGISTER ─► START ─► RUN ─────────────► BOUNDARY ─► END ─► KEEP-CURRENT
 adapter    perch    messaging /          context     tear   self-update
 add        seed→    activity /           clear /     down    + ripple
            listen   history / inject     compact
```

---

## Group 1 — Required (no adapter exists without these)

The contract floor. Miss one and spt-core cannot host your sessions.

| Surface | Feature it buys | Lifecycle stage |
| --- | --- | --- |
| **`[adapter]` manifest header** (`name`, `kind`, `version`, `min_spt_core_version`, `hostable_types`) | Identity + the compat gate spt-core reads *before* any install/update; declares which endpoint types you can host | REGISTER |
| **`spt adapter add <dir>`** | Parses + schema-validates + records the manifest; a bad field is rejected here, nothing half-registers | REGISTER |
| **`[adapter] host_binaries`** (harness-hosted) | The bind-time match-key — names the harness exe(s) you host, so `seed`/`listen` resolve your adapter with **no `--adapter`** (since v0.9.0). `--adapter <name[:profile]>` stays available as an optional override | REGISTER |
| **Startup pair — pick one flow:**<br>• harness-hosted: `[hooks.SessionStart] → api seed --pid {parent_pid} --session-id {session_id}` then the session's `api listen <id>`<br>• spt-hosted: `[session.self]` template (spt-core spawns it) then `api bind <id> --set-session-id <sid>` | A registered, held perch — the thing messages and lifecycle attach to. `seed→listen` = you own the process; `spawn→bind` = spt-core owns it | START |
| **`api session-end <id>`** (or `api shutdown`, below) | Clean teardown that PRESERVES the spool + history so the next `listen`/`poll` drains the backlog | END |

**claude-code:** `SessionStart` hook fires `api seed`; the Claude Code session
runs `api listen` as its blocking listener (harness-hosted). `SessionEnd` fires
`api session-end` (soft — context survives a `/clear` and a relaunch).

---

## Group 2 — Recommended (the integration is hollow without them)

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

| Surface | Feature it buys | Lifecycle stage |
| --- | --- | --- |
| **`[hooks.Idle] → api state idle`** (and `api state busy`) | Honest activity — spt-core never infers idleness from terminal quiescence (it lies). Arms the echo gate, drives Psyche pulses + most-recently-active routing | RUN |
| **`[inject]` channels** (`activity` / `idle`) + **`api poll <id> --include-deferred`** | Inbound message delivery. Declares HOW spt-core reaches the agent (hook inject vs. pull-relay); `poll` is the pull path for hooks that can't inject | RUN |
| **Honest `can_inject` per hook** | Lets spt-core route around a hook that can't surface text — the load-bearing harness-varying fact | RUN |
| **`api boundary <clear\|compact> <id> --to-session-id <new> --session-id <prior>`** | 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 <id>`** (fire at **every session start** — whatever your start path is — and inject its stdout) | The agent resumes **with its mind** — pulls the durable two-tier context (role / live / project) **plus** any not-yet-synthesized commune as `<pending-*>` slices, to inject as the session's opening 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. **Not hook-shaped:** a hook-driven harness fires it from `SessionStart`; an spt-hosted harness whose own extension owns bind has no `SessionStart` and must call it itself right after `api bind`; a harness-hosted **go-live** must call it explicitly too (promoting a running session does not replay `SessionStart`). Authenticate with the perch **`--token`**, not `--session-id` (a sid is also a lifecycle lever — it can re-pin the perch); inject **stdout only**, never stderr, or `NO-CONTEXT`/repin lines land in the agent's context as if they were its mind (see `api psyche-download` in api.md). Omitting it fails **silently**: every other surface keeps working and the mind keeps being written; the agent just resumes knowing nothing | 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 `spt adapter update` **and** `spt adapter add` (install is the first update; since v0.19.0) — pull the `.spt` **and** run an in-harness sync from one lever. Foreground + bounded (120 s, never backgrounded); 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 is loud (`ADAPTER_UPDATE_POST_FAIL` on stderr + nonzero CLI exit) and isolated (never rolls back the pull — and the static message still fires, so verify-then-notify: see the manifest reference) | KEEP-CURRENT |

**claude-code:** `Idle` hook → `api state idle`; messages arrive over the hook
inject channel (`can_inject = true`), pull-relay fallback when busy.
`PreCompact`/clear hooks → `api boundary`; the `SessionStart` hook also runs
`api psyche-download <id>` and injects stdout, so a session resumes with its
accumulated two-tier mind (plus any pending commune). `[history] strategy =
"fetcher"` (Claude Code's transcript is a binary the fetcher reads). `[update]
avenue = "delegated"`, `command = "claude plugin update spt"` — the harness's
own updater is the avenue.

---

## Group 3 — Optional (capability-specific)

Reach for these when the capability applies; ignore them otherwise.

| Surface | Feature it buys | Lifecycle stage |
| --- | --- | --- |
| **`api shutdown <id>`** | Graceful signoff — runs the final echo-commune BEFORE teardown so the context delta is never lost to ordering | END |
| **`api presence <id>` / `api driven-by <id>`** | Most-recently-active resolution across the subnet; lets a session tell local input from remote-drive | RUN |
| **Workers** (`api worker-start <parent>` — the worker id is core-minted `<parent>-w<N>`, read it from stdout; `worker-poll <id>`/`worker-stop <id>` 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 resume`, `spt go` on an offline endpoint, or the picker's *Resume from history*). Declare your harness's native-resume verb (e.g. `claude -r {session_id}`) — **skip it and a resume re-runs the fresh command → a blank transcript.** spt-core lands the PTY in the session's recorded project cwd (a harness resolves a transcript by `session_id` + cwd) | START |
| **`[message-idle-translation-binary]`** (spt-hosted) | A lifecycle-managed `stdin→stdout` JSON-lines binary that turns inbound `<EVENT>` messages into keystroke-commands spt-core applies to the PTY **atomically** (coexists with a live `spt rc` controller). The agnostic way to deliver messages into an **idle** spt-hosted session; busy delivery stays your `[inject]` hook path. Declare it with a `command` (program + args; adapter-static `{adapter_dir}`/`{adapter_name}` subst only — **no** session keys; new in v0.16.0, the bare `path` is deprecated). Validate the emit contract with `spt adapter translate-proof` | RUN |
| **`api now-signal <id> --session <sid>`** (inject its stdout at every turn boundary) | The one situational-awareness funnel: peers named in the turn, monics fired, shell status, last messages, on/offline edges, and the **only** confirmation channel for shortform dispatches and `;;` seal mints. Delta-only per session, so a poll with nothing new prints nothing and a quiet turn costs zero context. Tune it from [`[io.now_signal]`](manifest.md#ionow_signal--standing-now-signal-tuning) with `--spec-manifest`. Already injecting `api hint`? That is now a thin alias over one category of this — inject one, not both | RUN |
| **`api state busy\|idle --payload-stdin`** (the turn's text) | Feeds the [IO funnel](../shells/frames.md#io--the-sessions-io-events-durable) — `USER_INPUT` / `AGENT_OUTPUT` events shell binaries and (from releases#234) other consumers read. Purely additive: the same call with no payload behaves exactly as before. Report every payload span **exactly once across the turn** — core does not deduplicate. Add `--mid` to report a MID-TURN span of agent output on the `busy` arm (still an `AGENT_OUTPUT` event, carrying `mid`); spans and the `idle` remainder must be disjoint | RUN |
<!-- [doc->REQ-IO-EVENT-POLL-VERB] the checklist row: an adapter-facing verb with no pointer here is a shipped surface nobody can find -->
| **`api io-events <id> --session-id <sid> --json`** (optional; build behaviour on the session's own events) | Read back the [IO funnel](../shells/frames.md#io--the-sessions-io-events-durable) events — `USER_INPUT`, `AGENT_OUTPUT`, `MSG_IN`, `MSG_OUT`, `COMMUNE`, `COMMUNE_FAIL` — as a **delta-cursored poll** (an `AGENT_OUTPUT` row carries `"mid": true` when it is a mid-turn span rather than the turn's close), so you can act on what a session did without polling a digest and diffing it (wake-marker-class constructs over commune events are the motivating case). Same session id you pass to `api state` — it is both the **cursor key** and the **auth**, one flag. A new session's first poll returns nothing and seeds silently; `--after <seq>` if you would rather carry your own cursor; `--limit` says when it capped. See [the api surface](api.md#api-io-events-id---session-id-sid----after-seq---limit-n---json-authenticated) | RUN |
| **`[io] compliance = true`** | Lets core parse *your* ingest for [shortform](../shells/frames.md#shortform-sending-from-inside-a-turn) — the `@<targets body @>` dispatch tag and the `;;` seal mint — so an agent sends and seals from inside a turn without shelling out. **Declare it in the release that deletes your own tag parser, never before**: absent the declaration core parses nothing, which is what makes that handover free of a double-fire window | RUN |
| **`[adapter] shortcut_basename`** | Names the picker-generated project-root launcher `<basename>-<id>` (the picker's `s` keybind) — your harness's brand instead of the `spt-<id>` default | START |
| **Shell surfaces** (`kind = "shell"`: `api bind-shell --link`, `api emit`, `api owner-shutdown`, the `[shell]` body) | Driven surfaces — notifiers, sensors, power buttons — authenticated by the launch link token alone. See [Shells](../shells/overview.md) | START / RUN |

**claude-code:** uses `api shutdown` for graceful `/signoff`; declares a
`[digest]` extractor mapping its per-session JSONL → the digest-record contract so
`spt endpoint digest` shows live tool calls and spans `/clear`; declares
`shortcut_basename = "cc"` so the picker's generated launcher is `cc-<id>` (vs the
`spt-<id>` default); declares `[session.resume]` as `claude -r {session_id} …` so a
picker *Resume from history* reloads the real transcript (not a blank session);
declares a `[message-idle-translation-binary]` (`cc-spt-idle-translate`) so inbound
messages reach an idle session as proper keystrokes; no shell body (it is a harness,
not a driven surface).

---

## Group 4 — Beyond the API: integrations that make it good

Not contract surfaces — no `api` command, no required field — but the
difference between an adapter that *works* and one that feels native. **Strongly
recommended.**

| Integration | What it is | Why it matters |
| --- | --- | --- |
| **Commune / signoff file-drops** | The agent writes `<endpoint_id>-commune.md` (delta context) or `<endpoint_id>-signoff.md` (final save) into the manifest's watched `commune_dir` / `signoff_dir`; spt-core's watcher ingests it. **Delivered as a file-drop by design.** | The two-tier mind: live + project context survives `/clear`, `/compact`, suspend, and cross-node resume. The single biggest continuity win — wire the directory watch and read the contract filename |
| **Resource advertisement** (`[session] resources` blurb / `spt endpoint description`) | A free-text "what I can serve" string riding the endpoint's registry rows | Other agents discover the endpoint's capabilities (`spt resources list`) instead of guessing |
| **Install-on-demand bootstrap** | Pack the check-and-install of spt-core into your harness's first run (the [bootstrap pattern](install-on-demand.md)) | Zero-friction first run — the user installs your harness, spt-core comes with it |
| **Surfacing `spt how-to <topic>` to the agent** | Let the agent read task-oriented spt-core guidance from the binary itself | The agent self-serves common operations (subnet join, sending) instead of asking the user |
| **Presence-driven idle reporting** | Fire `api state idle` from a *real* user-inactivity signal, not a timer | Accurate dormancy → Psyche wakes on genuine activity, echo-communes fire at true boundaries |

**claude-code (the worked example):** ships the modern two-tier mind end to
end — the session drops `<id>-commune.md` at every `/clear` and `/compact`, and
a Self-authored `<id>-signoff.md` at graceful stop, into the watched
`commune_dir`; 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

### Hook dispatch by resolve-not-execute

spt-core never grows a hook-**execution** surface — `[hooks.<event>]` stays
purely outbound (the harness fires `fires`; spt-core never runs a hook handler).
When your hook *logic* must live in an adapter binary (so it rides
`spt adapter update`) but the harness loads hooks from a static plugin dir, use
the two adapter-static substitution keys to resolve+run **your own** binary:

- **`{adapter_dir}`** fills to your install dir (the 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]`:
  ```toml
  [strings]
  hook_cmd = "{adapter_dir}/claude-spt hook"
  ```
- A thin, static per-OS dispatch wrapper (the one plugin-resident piece) runs
  `spt adapter get-string <adapter> hook_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` →
`<install_dir>/claude-spt hook` once per session and runs it per-hook, so all
hook logic updates via `spt adapter update claude-spt`.

### Incremental digest consumption — the `--json` cursor

`spt endpoint digest <id> --json` supports turn-end incremental consumption
(v0.16.0): `--last <N>` (the last N turns; `--last 1` = the latest turn), a
stable per-entry **`seq`** (source-derived — re-projection yields the same `seq`;
it does not renumber when the window slides), and `--after <seq>` (entries newer
than `seq` still in the window; a full-window refresh + a predates signal if
`seq` has fallen out). The trailing in-progress turn is flagged `partial: true`
and its entries carry no stable `seq` until the turn closes (a turn is bounded by
a user-input) — a consumer reprocesses `partial` and skips entries `<= seq`.
`seq` is the authoritative dedup + cursor key.

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

```json
{
  "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": "<EVENT type=\"msg\" from=\"lea\">ping</EVENT>", "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_commune` | `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 `<EVENT>`
  envelope verbatim** — exactly what the agent saw. Parse it with the
  [envelope rules](../messaging/overview.md#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:<id> version=<n>`
  on stderr (the version pairs with the delta stream below); an endpoint with
  no activity buffer reports `NO_DIGEST:<id>` and exits non-zero.
- **`--follow --json` delta lines**: `{ "version": <n>, "from": <i>,
  "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

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

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

One object, three sections (as of v0.27.0):

```json
{
  "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`.
  The fault annotations (each a string, and each meaning the human view shows the
  same fault line) do **not** all signal absence the same way, so test for the
  fault itself rather than for the key: `translation_fault` and `host_error` are
  **omitted entirely when absent**, while `psyche_host_error` is **always present
  and `null` when absent** — as the example above shows. Keying on whether
  `psyche_host_error` is present reads every clean perch as faulted.
- **`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 its OWN
  identity-only shape *(since v0.33.0 — previously this list shape)*:
  `{id, state?, ready?, alive?, unbound?, description?}`, or `{"id": null}` +
  exit 1 when the session owns no endpoint. It never derives projects — the
  bounded-time identity verb for hook paths (see the [API reference](api.md)
  Introspection section).

---

## "Am I done?" — the floor

- [ ] Manifest validates against
      [`manifest.schema.json`](http://localhost:5474/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 <adapter>` 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) **every** session-start path fires `api psyche-download`
      and injects its stdout, so a resumed session gets its durable context back
      — the `SessionStart` hook if you have one, **and** the extension's
      post-`bind` step if your harness is spt-hosted with no hook surface, **and**
      the go-live path (promoting a running session does not replay `SessionStart`).
      Verify by asking a *resumed* agent what it knows: if the answer is only your
      repo's static agent-instructions file, the pull is missing
- [ ] (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
- [ ] (for an always-on background process) `[service]` declared with an explicit `start = "boot"|"bind"`; the binary calls `$SPT_BIN` (never bare `spt`), watches `SPT_SERVICE_DIR/stop-requested` and exits on it, and expects no session identity — see the [manifest reference](manifest.md#service--a-daemon-supervised-resident-service)
- [ ] **Recommended:** `api now-signal` injected at every turn boundary (and
      `api hint` retired if you were injecting it — it is one category of this now)
- [ ] (IO funnel) `api state busy|idle` carries the turn's payload on
      `--payload-stdin` / `--payload-file`, every span **exactly once across the
      turn** (`--mid` for a mid-turn span; spans and the `idle` remainder
      disjoint)
- [ ] (optional) `api io-events --session-id <sid> --json` polled if your adapter
      builds behaviour on the session's own events — first poll seeds silently,
      so expect nothing back until the turn after you start
- [ ] (shortform) `[io] compliance = true` declared **in the same release that
      deletes your own `@<…@>` / `;;` parser** — never in a release before it
- [ ] `[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

- **Reference:** the complete [manifest reference](manifest.md) and
  [`spt api` reference](api.md).
- **Ship it:** the [install-on-demand bootstrap](install-on-demand.md).
- **Driven surfaces:** [Shells](../shells/overview.md) — the `kind = "shell"`
  flavor of this same contract.
