# 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 <name>` on every `api` call** | Multi-harness disambiguation — the rule that keeps a two-harness node unambiguous | every stage |
| **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 <sid>`** | The endpoint's identity, spool, and history survive a context reset under a new session id | BOUNDARY |
| **`[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 |

**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.

| Surface | Feature it buys | Lifecycle stage |
| --- | --- | --- |
| **`api shutdown <id>`** | Graceful signoff — runs the final echo-commune BEFORE teardown so the context delta is never lost to ordering | END |
| **`api presence <id>` / `api driven-by <id>`** | Most-recently-active resolution across the subnet; lets a session tell local input from remote-drive | RUN |
| **Workers** (`api worker-start <parent> <id>`, `worker-poll`, `worker-stop`) | Nested, short-lived sub-agents under a parent endpoint | RUN |
| **`[digest]` extractor** (or `api digest-entry`) | A live activity digest (`spt endpoint digest`) — declare an extractor mapping your native log → the `{role, text, tool, ts}` contract (ADR-0019; its OWN seam, no longer riding `[history]`). Spans `/clear` via the session ledger; validate with `spt adapter digest-proof` | RUN |
| **`[session.notif]` template** | Native OS notification render (toast / shell alert) for consent + capability prompts, instead of burying them in agent output | RUN |
| **`[adapter] shortcut_basename`** | Names the picker-generated project-root launcher `<basename>-<id>` (the `spt endpoint run` `s` keybind) — your harness's brand instead of the `spt-<id>` default | START |
| **Shell surfaces** (`kind = "shell"`: `api bind-shell --link`, `api emit`, `api owner-shutdown`, the `[shell]` body) | Driven surfaces — notifiers, sensors, power buttons — authenticated by the launch link token alone. See [Shells](../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); 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`; 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`](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)
- [ ] 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

- **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.
