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

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.

[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 --name {session_name} --model cheap"
cwd = "{psyche_dir}"
env_remove = ["MY_HARNESS_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"
detach = true
keys = ["session_name", "psyche_dir"]
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.

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

[pty_digest] — live activity buffer patterns

For sessions spt-core hosts in its own terminal layer, the adapter may supply regex patterns that segment the raw PTY byte stream into an at-a-glance activity digest (spt digest <id>). The patterns are opaque to spt-core — the same rule as command templates; spt-core compiles and runs them, never understanding harness semantics.

[pty_digest]
input_pattern = "\\nUSER> "      # user-turn boundary (required)
agent_pattern = "\\nAGENT> "     # agent-turn boundary (required)
tool_patterns = [                 # optional: one regex per known tool,
  "(?P<name>Write)\\((?P<arg>[^)]*)\\)",   # naming `arg` (+ optional `name`)
]
catchall_pattern = "(?P<name>[A-Z][a-zA-Z]+)\\((?P<arg>[^)]*)\\)"  # optional
window_turns = 3                  # optional presentation override
persist = false                   # opt-in coarse activity log (default off)

Both boundary markers are required when the section is present; an absent section just means the digest is unavailable for this adapter.

[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.
  • [env.*] direction = "inject" requires a value.
  • [update] avenue = "delegated" requires command; file_pull requires repo and signing_key.
  • [pty_digest], when present, requires non-empty input_pattern and agent_pattern.

A violation is a one-line error naming the field — fix and re-add.