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.
| 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: • 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 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 | 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. 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 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) | 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 -
[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
apicall carries--adapter <name> -
api state idlefires on real inactivity;can_injectvalues are honest - An inbound delivery channel is declared (
[inject]) or pulled (api poll) -
[history]strategy chosen;api boundarywired for clear/compact - (for a live digest)
[digest]extractor declared +digest-proof-checked, orapi digest-entrypush -
[update]avenue declared (ripple-update + install-on-demand) - Teardown fires
api session-end(orapi shutdownfor graceful signoff) - Recommended: commune/signoff directory watched (mind continuity)
-
spt adapter add ./your-adapterregisters clean;api … capabilityechoes yourhostable_types
Next
- Reference: the complete manifest reference and
spt apireference. - Ship it: the install-on-demand bootstrap.
- Driven surfaces: Shells — the
kind = "shell"flavor of this same contract.