# Runtime manifest schema

> Draft design doc (2026-05-31). Resolves PRD §17.2. Implementation detail — see [`../CONTEXT.md`](../CONTEXT.md) for the model. Schema grounded in the sister project's actual functional surface.

## Principle

**SPT is not a harness.** The manifest declares only what *varies per 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 just runs it with substitution keys filled. Anything spt-core owns (sentinels, spool/registry/perch schemas, the event-block vocabulary, the version-trampoline mechanism, config knobs) is **not** in the manifest.

## What is NOT in the manifest (spt-core-owned)

- **Sentinels** (`.idle`-equiv, echo-gate) — managed by `api state`/`api echo-gate`; the adapter only *calls* them.
- **Spool / registry / perch `info.json` / daemon-state schemas** — core.
- **Event-block vocabulary** — the XML tags spt-core surfaces to agents (e.g. `<SPT_EVENT>`, `<owl_messages>`-equivalent) are a **fixed, documented constant**, not configurable. 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 (in `[session]`).
- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.
- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.
- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).

<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the
typed form in `crates/spt-runtime/src/manifest.rs` is kept in lockstep. -->

## Manifest sections

### `[adapter]` — header
```toml
[adapter]
name = "claude-spt"
version = "2.0.0"
min_spt_core_version = "1.0.0"     # readable before any update; compat gate
hostable_types = ["LiveAgent", "ReadyAgent", "Psyche", "Worker"]
shortcut_basename = "cc"           # optional; brands the picker's launcher → cc-<id> (default "spt")
host_binaries = ["claude"]         # optional; the harness exe(s) this adapter hosts agents inside
```

<!-- [doc->REQ-MANIFEST-7] -->
`shortcut_basename` *(optional, default `spt`)* — the basename the `spt endpoint
run` picker's `s` keybind bakes into the generated `<basename>-<id>` launcher
shortcut at the project root (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic
`spt` (→ `spt-<id>`); an adapter sets it to brand its shortcuts — `claude-spt`
uses `cc`, giving `cc-doyle`. spt-core never hardcodes a harness name; the
picker reads this from the **resolved** manifest of the selected adapter. The
launcher is the current OS's native form (`.cmd` on Windows — `.ps1` is excluded
by the default `PATHEXT`; a POSIX `sh` `+chmod +x` on Unix) and overwriting is
guarded by a generated-by sentinel (a foreign same-named file is never clobbered).

<!-- [doc->REQ-MANIFEST-8] -->
`host_binaries` *(optional, since v0.9.0)* — the harness executable basenames a
`kind="harness"` adapter hosts agents inside (e.g. `["claude"]`). This is the
bind-time **match-key**: a harness-hosted session's parent process maps to its exe
basename, which selects the adapter(s) that declare it. **Match rule:** both the
declared entry and the live exe basename are normalized to **lowercase + the stem
before the first dot**, then compared — so `claude` matches `claude`, `claude.exe`,
`claude.cmd`, *and* the renamed-in-use form `claude.exe.old.<timestamp>` (a harness
self-update can rename its still-running exe, e.g. Claude Code's updater, so a
long-lived session's exe basename is `claude.exe.old.<ts>`). **A declared
host-binary name must therefore not itself contain a dot** (real harness exe names
don't). With `host_binaries`, `spt api seed` / `listen` need **no `--adapter`** —
the owning adapter is resolved at bind. Declare it once per harness binary you host;
an adapter author then runs `spt adapter use <adapter>[:profile]` to pick the active
default when more than one adapter hosts the same binary (without a pick, the
freshest-registered hosting adapter wins). Omitted ⇒ the adapter is reachable only
via an explicit `--adapter`.

### `[hooks.<event>]` — inbound hook table
Per harness event: the `api` command it fires, the stdin fields it maps in, and its **output capability** — `can_inject` (whether this hook can surface context to the agent). When `can_inject = false`, spt-core falls back to a sentinel + relay/poll path instead of expecting injection. This is the load-bearing harness-varying fact (CC's Stop hook can't inject).

```toml
[hooks.SessionStart]
fires = "api seed --pid {parent_pid} --session-id {session_id}"   # adapter-agnostic since v0.9.0; no --adapter
reads = ["session_id", "source", "agent_type"]
can_inject = true                  # via additionalContext

[hooks.Stop]
fires = "api state idle"
reads = []
can_inject = false                 # no hookSpecificOutput schema → sentinel/relay fallback
```

### `[session.<role>]` — outbound session role templates
One opaque command template per role: `self`, `resume`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

<!-- [doc->REQ-INSTALL-11] -->
**Install-dir program resolution (since v0.8.0).** A command template's bare program token (its first whitespace-split token, e.g. `claude-spt-digest …`) resolves against the **adapter's install dir** before PATH, so a `.spt` that ships its own binaries is self-contained — no PATH placement needed. spt-core probes `<install_dir>/<program>` (on Windows also the `.exe` suffix); when that file exists it runs that absolute path, otherwise it leaves the bare token for PATH to resolve. The install dir is the registry record's `source_dir` — `adapters/_github/<safe>/` for a `--release`/`--github` adapter, the copy-mode `source_dir` otherwise. This covers the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool. (The `spt` binary itself still resolves from PATH the usual way.)

```toml
[session.psyche_init]
command = "claude -p --agents {agents_json} --agent owl-psyche --name {session_name} --model sonnet --fallback-model opus --effort medium --dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write --output-format json"
cwd = "{psyche_dir}"
env_remove = ["OWL_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"   # set on summarizer children so their own hooks bail
detach = true
keys = ["agents_json", "session_name", "psyche_dir"]   # substitution keys spt-core fills
```
(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)

<!-- [doc->REQ-SESSION-RESUME-TEMPLATE] -->
**`[session.resume]` — the agent's own-session NATIVE resume.** `self` is the
agent's own session; `resume` is its native-resume sibling (mirroring
`psyche_init`→`psyche_resume`). spt-core selects `[session.resume]` over
`[session.self]` **only** when a bringup carries a prior session (`spt endpoint
run --resume <session>`, or the picker's *Resume from history*) **and** the
adapter declares the role. An adapter with **no** `[session.resume]` falls back
to `[session.self]` (full back-compat) — but then a resume re-runs the *fresh*
command (e.g. `claude --session-id …`) and the harness starts a blank
transcript. A harness with a native-resume verb (Claude Code: `claude -r
<session_id> …`) must declare `[session.resume]` to resume into existing
history. Keys spt-core fills are the SAME catalog as `self`: `{id}`,
`{session_id}` (the **resumed** id), `{session_name}`, `{adapter_name}`. The
harness resolves a transcript by `session_id` **+ cwd**, so spt-core also lands
the resumed PTY in the session's recorded **project cwd** — the per-session
ledger row's `cwd` → else the perch `info.cwd` → else the current dir (so a
single-project endpoint and pre-migration ledger rows still resume correctly).

```toml
# Claude Code: native resume into an existing transcript by id, in its project cwd.
[session.resume]
command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]
```

**Substitution-key catalog.** The full `{key}` vocabulary spt-core guarantees it can fill — a role's `keys` must be a subset (source of truth: `spt_runtime::SUBSTITUTION_KEYS`): `{id}`, `{adapter_name}`, `{session_id}`, `{session_name}`, `{parent_pid}`, `{agent_type}`, `{agents_json}`, `{psyche_dir}`, `{psyche_prompt}`, `{psyche_context}`, `{link_token}`, `{source}`. spt-core fills only the keys relevant to a given spawn (`{psyche_*}` for a live agent's Psyche role, `{source}` for a `[digest]`/`[history]` extractor, …); an unknown or unprovided `{placeholder}` fails with a one-line error. Per-key meanings: the published [manifest reference](../docs-site/src/harness-contract/manifest.md#substitution-keys).

<!-- [doc->REQ-NOTIF-2] -->
`notif` is the **endpoint-native notification render** (ADR-0007's `notif_command` seam): an OS toast, a Shell `alert-symbol`, anything the adapter can run. Declared on harness-adapter **and** shell-adapter manifests; spawned **detached** when a notif surfaces at this endpoint, combinable with the agent-surface delivery (the typed `notify` envelope). Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.

```toml
[session.notif]
command = "powershell -Command New-BurntToastNotification -Text '{notif_from}','{notif_body}'"
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
```

### `[env]` — env-var table
Vars to inject/read, and **how**. With `spt` on PATH the table is small. The injection *channel* is asymmetric: **spt-hosted** sessions inherit env from the broker that spawns them (no channel needed); **harness-hosted** sessions need the harness's declared channel (the broker didn't spawn them). File-bridge-only-when-not-launcher, applied to env.

```toml
[env.OWL_SESSION_ID]
direction = "inject"
value = "{session_id}"
channel = "CLAUDE_ENV_FILE"        # harness-hosted only; spt-hosted inherits from broker
```

### `[history]` — transcript access
Three strategies; pick one. Pull-based (spt-core asks) or push-based (adapter sends).
```toml
[history]
strategy = "fetcher"               # fetcher | locate_normalize | native
fetcher = "claude-spt-history --session {session_id}"   # adapter binary emits normalized history
# locate_normalize: locate_template + normalize_command
# native: adapter pushes via `api history-log`; spt-core stores (also used for Shells)
```

### `[digest]` — session-digest extractor (ADR-0019)
The session digest's own seam — **distinct from `[history]`** (which stays opaque
and single-session, feeding the echo-commune verbatim). Declares an **imperative
extractor** that maps the harness's native log → the digest-record contract.
```toml
[digest]
extractor = "claude-spt-digest --session {session_id} --in {source}"  # native log → contract JSONL
source = "~/.claude/projects/{project}/{session_id}.jsonl"   # optional; defaults to [history].locate_template
window_turns = 5                   # optional presentation defaults the adapter declares…
arg_truncation = 40                # …any consumer may override at pull/subscribe
sprint_collapse = true             # collapse consecutive tool records into one sprint
```
- **`extractor`** (required) — an opaque command spt-core never parses. spt-core
  resolves the `source` file, fills `{source}` with its path, pipes the bytes on
  stdin, and runs the command bounded; its stdout is contract JSONL (one record
  per line). Why imperative, not a declarative DSL: real harness logs are nested
  (one line → many entries, mixed block lists, types to filter); a flat map can't
  express them and a map powerful enough is a reinvented language.
- **`source`** (optional) — own-source escape hatch; absent, reuse
  `[history].locate_template` (DRY). A **log-less** adapter declares no `[digest]`
  and pushes via `api digest-entry` instead.
- **presentation knobs** (`window_turns`, `arg_truncation`, `sprint_collapse`) —
  adapter-declared **defaults** any consumer may override; spt-core ships
  fallbacks (`3` / `25` / collapse-on).

Prove your extractor before shipping: `spt adapter digest-proof <adapter>
--sample <real-log>` runs it and prints the parsed records, the rendered digest,
and **every dropped line with its reason** — no silent empty.

### `[strings]` — adapter string values (M9 / file-backed M12-W3)
An adapter-authored KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter[:profile]> <key.path>` (data only — never executed; node-local). Resolves through the same leaf-replace profile overlay as the rest of the manifest. A value MAY be an inline literal **or** a file pointer — a value-position table with **exactly one** key `file`:
```toml
[strings]
greeting = "hello"                       # inline
skills.whoami = { file = "whoami.md" }   # pointer → get-string returns the file's CONTENTS
```
Pointer files live in `adapters/<adapter>/strings/`, referenced by a path that **must stay inside** that dir (`..`/absolute refused at registration). Validated at register (fail-fast), read lazily at get-string (live edits reflect), skip-diagnoses a missing file at read (mirrors `[digest]`). A **local** profile's pointers resolve against the user-owned local-profile dir (update-safe). Published reference: [manifest.md `[strings]`](../docs-site/src/harness-contract/manifest.md#strings--adapter-string-values--profiles).

### `[inject]` — inject-input methods
Per activity-state, any combination: `pty`, `hook`, `relay`, `http`.
```toml
[inject]
activity = ["hook"]                # non-disruptive during work
idle     = ["pty", "hook"]
```

<!-- [doc->REQ-MSG-IDLE-TRANSLATION-BINARY] -->
### `[message-idle-translation-binary]` — spt-hosted idle delivery (ADR-0022)
Opt-in. The adapter's **idle-delivery translation binary**: a pure stdin→stdout
JSON-lines filter spt-core lifecycle-manages (spawned when the spt-hosted endpoint
comes up, terminated when it goes down). spt-core feeds it the inbound `<EVENT>`
feed and reads back keystroke-commands, which spt-core applies to the broker-held
PTY **atomically** — controller input is buffered during the emitted sequence and
flushed after, so injection coexists with a live `spt rc` controller (spt-core owns
every PTY write). Idle-only; busy/mid-turn delivery stays adapter hook-injection.
A **table** carrying a `path` scalar (not a bare top-level scalar — a table can't be
silently absorbed by a preceding section, and it stays extensible).
```toml
[message-idle-translation-binary]
path = "cc-spt-idle-translate"     # the binary spt-core spawns + drives
```
- **stdin** (spt-core → binary, one JSON object per line): `{"type":"init","endpoint_id":…,"node":…}` first · `{"type":"event","envelope":"<EVENT…>"}` per inbound message (the ADR-0020 envelope) · `{"type":"input"}` a **content-free** ping each time the operator types (so the binary can track user-idle; the PTY input content is **never** duplicated to the binary).
- **stdout** (binary → spt-core, one per line): `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<payload>"}` · `{"key":"enter"}`, … (extensible vocabulary).
- **Known keys today:** `path`. spt-core does **not** `deny_unknown_fields` here — a newer adapter declaring a future key against an older spt-core parses fine (the unknown key is ignored), so the lifecycle-binary contract degrades gracefully.
- The v0.11.0 raw `payload+\r` inject is the **degenerate** case (a binary that emits `{"text":payload}{"key":"enter"}` with no choreography).

### `[session]` watched dirs (file-drop)
```toml
[session]
commune_dir = ".claude"            # watched for <id>-commune.md (filename fixed by spt-core)
signoff_dir = ".claude"            # watched for <id>-signoff.md
```

### `[identity]`
```toml
[identity]
session_id_source = "post_spawn"   # post_spawn | uuid_inject
parent_ancestor_name = "claude"    # process-tree anchor when session_id absent
```

### Session digest — the published digest-record contract (ADR-0019)

<!-- [doc->REQ-TERM-5] The session digest is a PROJECTION of the endpoint's
session logs, never a PTY-byte parse (the superseded source mechanism). ADR-0019
gives it its OWN manifest seam — the `[digest]` extractor above — distinct from
`[history]` (which stays opaque + single-session, feeding the echo-commune
verbatim). The M9 "no manifest seam / rides `[history]`" stance is REVERSED: one
`[history]` normalizer cannot serve both the opaque echo consumer and the
contract-typed digest. What is published here is the digest-record CONTRACT: the
small, fixed, spt-core-owned shape the `[digest]` extractor emits (or that
`api digest-entry` pushes). -->

The `[digest]` extractor (or an `api digest-entry` push) emits the digest-record
contract — a JSON object per record:

```json
{"role": "input", "text": "add a file", "ts": "2026-06-13T21:00:00Z"}
{"role": "agent", "text": "on it"}
{"role": "tool",  "tool": {"name": "Write", "arg": "src/a.rs"}}
```

- `role` — the closed set `input` | `agent` | `tool` (the source tag).
- `text` — the input / agent span (omitted/empty for a `tool` record).
- `tool` — `{name, arg}`, present **iff** `role == "tool"`. Consecutive `tool`
  records collapse into one sprint (unless `sprint_collapse = false`).
- `ts` — optional RFC3339-UTC ordering key; the two-origin merge interleaves
  extracted activity with spt's own injected-context entries by it.

**Forward-compat:** unknown object fields are ignored; a line that is not a valid
record (malformed JSON, missing/unknown `role`, a `tool` record without its `tool`
object) is **dropped with a counted reason** — never silently, never fatal. Run
`spt adapter digest-proof` to see exactly what dropped and why.

**Presentation is spt-core's** (window depth + arg-truncation + sprint-collapse,
adapter-defaulted via `[digest]`, consumer-overridable); **extraction is the
adapter's** (the `[digest]` extractor → the record contract).

Two access modes over the digest control channel: a **snapshot pull** (`spt
endpoint digest <id>` — projected on demand) and a **structured-delta stream**
(`spt endpoint digest <id> --follow` — changed turns pushed; deltas driven by
pulls / `api digest-entry` pushes). Autonomous file-watch freshness is deferred to
the consuming frontend milestone. Local addressing only.

### `[update]` — adapter self-update
```toml
[update]
avenue = "delegated"               # delegated | file_pull | gh_release
command = "claude plugin update spt"          # delegated
# file_pull: repo + path_regex
version_check = true               # verify spt-core satisfies min_spt_core_version before/after
uninstall = "claude plugin uninstall spt"     # optional; mirror of install — runs on `adapter remove` once quiesced
```

<!-- [doc->REQ-UPD-9] -->
The **`gh_release`** avenue (since v0.8.0) ships the adapter's updates from its own GitHub releases — no signing tooling and no plugin coupling required of the author:

```toml
[update]
avenue = "gh_release"              # ship updates from this repo's GitHub releases
repo = "user/repo"                 # REQUIRED: whose releases ship updates
asset = "adapter.spt"              # optional; the release asset to fetch (default adapter.spt)
signing_key = "deadbeef…"          # optional Ed25519 (64 hex) — enables fail-closed verify
```

`spt adapter update [name]` (with no name, every registered `gh_release` adapter; with a name, just that one) compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers it in the adapter's durable `_github/<safe>` home (pointer-mode, re-read live). The network fetch lives in the `spt` CLI, never the daemon. **`repo` is required**; `asset` defaults to `adapter.spt`; `signing_key` is **optional**.

**Trust — optional signing, fail-closed.** With no `signing_key`, the fetched `.spt` is trusted on HTTPS + GitHub (the same first-acquisition trust `spt adapter add --release` and the installer's first binary fetch place). With a `signing_key`, the fetched `.spt` is verified against a **detached signature** the author publishes as a sibling release asset named `<asset>.sig` — a lowercase-hex Ed25519 signature over the raw archive bytes. Verification runs **after** the bytes are staged and **before** they are extracted/registered, against the **installed** manifest's key (key continuity: a new `.spt` must verify against the key already on the node). A bad, mismatched, or missing signature **refuses** the update — the staged bytes are deleted and never extracted. The author signs their own releases with their own key; spt-core's release key stays scoped to spt-core.

Validation requires `repo` for `gh_release` (`asset` and `signing_key` optional). `spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for `file_pull`; for `delegated`, just deregister and leave the harness's own files).

---

## Shell adapters (`kind = "shell"`)

A second kind of adapter. Provides a `Shell` endpoint (a driven surface) rather than hosting agents. Same file location (`adapters/`), different manifest body.

```toml
[adapter]
name = "GameRobot"
kind = "shell"                     # vs "harness"
version = "1.0.0"
min_spt_core_version = "1.0.0"

[shell]
spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
broadcast = "subnet"               # subnet | same-node | none
command_receipt = "http"           # http | stdin | relay   (how it receives agent commands)
pre_close = "park-and-save"        # optional instruction sent to the binary on link-break
close_timeout_ms = 3000            # graceful termination window before force-close
persistent = true                  # auto-online whenever the owner endpoint is online
wake_command = "gamerobot-waker --link {link_token}"  # long-running wake-watcher run WHILE offline; exit(wake-opcode) ⇒ revive
can_shutdown = false               # if true, the shell may fire `api owner-shutdown` to suspend its owner directly
require_approval = "none"          # none | remembered | always — per-spawn user-approval gate (floor; node/endpoint may tighten)
max_instances_per_owner = 4        # optional cap on existing instances per owner (online+offline both count); omit ⇒ unlimited
over_cap = "reject"                # reject | approve — at the cap: refuse, or require per-spawn approval beyond it (doesn't raise the cap)

[shell.capabilities]               # the command vocabulary (agent→shell) — the toolset
move      = { args = ["direction", "distance"] }
gesture   = { args = ["name"] }
alert     = { args = ["symbol"] }
screenshot = {}

[shell.sensory]                    # the sensory vocabulary (shell→agent, REST-only)
types = ["image", "sound", "event"]   # "event" = arbitrary descriptive payloads
```

The capability + sensory vocabularies live **here**, not on the perch — spt-core resolves them by `adapter_name`. Command-delivery (`command_receipt`) reuses the agent inject-input modes. The shell binary binds via `api bind` (type=Shell, owner from the link) and pushes sensory via `api emit --type <t>`.

**Sleep/wake:** when offline (+ `wake_command` set), spt-core runs the wake-watcher (mutually exclusive with the shell binary); `exit(wake-opcode)` → wake resolution; crash-exit → respawn with exponential backoff + give-up (until the next relink/launch). **The wake opcode is exit code 86.** The wake-watcher template shares the spawn template's substitution catalog (`{id}`, `{adapter_name}`, `{link_token}`), but its `{link_token}` is minted *unparked* — an offline link has no live credential (the close retired it); a waker wakes by exit code, never by driving the link. `persistent` auto-onlines the shell with its owner. `can_shutdown` authorizes `api owner-shutdown`. Cross-node *fresh-spawn* to wake an owner requires the owner's `shell_wake_spawn_anywhere` settings flag (pre-consent).

**Instantiation governance:** `require_approval` gates `shell spawn` (reusing the consent grant store — `remembered` persists a grant via allow-always, `always` prompts every spawn); `max_instances_per_owner` + `over_cap` cap how many instances one owner may hold (online + offline both count toward the cap). An instance's **alias** is **not** a manifest field — it is per-instance runtime state on the shell perch's `info.json` (set at `shell spawn --alias` / `shell rename`), since the manifest describes the *adapter*, not individual instances.

## Worked example: `claude-spt` (today's behavior, expressed)

Maps the sister project's actual surface onto the schema.

| Concern | Today (modern SPT) | Manifest expression |
|---|---|---|
| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |
| `cc`/`cc <id>` launcher | hand-written shell wrappers | `[adapter] shortcut_basename = "cc"` → the picker bakes `cc-<id>` (spt-core default is `spt-<id>`) |
| Psyche init | `claude -p --agents … --model sonnet --fallback-model opus …` | `[session.psyche_init].command` (opaque) |
| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
| Own-session native resume | `claude -r <uuid> --remote-control <id> …` (resume into existing transcript) | `[session.resume]` + `{session_id}` (resumed id) + resumed-row cwd |
| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |
| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |
| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |
| Session digest | extract `.claude/…/<uuid>.jsonl` → `{role,text,tool,ts}` | `[digest].extractor` (own seam, ADR-0019; not `[history]`) |
| Commune/signoff | `.claude/<id>-commune.md` watched | `[session].commune_dir/signoff_dir`; filenames fixed |
| Env | `$OWL`/`$LIVE`/`OWL_SESSION_ID` via `CLAUDE_ENV_FILE` | `[env]`; PATH removes most; channel only harness-hosted |
| Identity | `OWL_SESSION_ID` + `claude.exe` ancestor walk | `[identity]` |
| Update | (today: cplugs) | `[update] avenue="delegated" command="claude plugin update"` |
| period / echo-window / route-guard / 24h refresh | hardcoded/flags | spt-core **global knobs** (per-endpoint override), not manifest |

## Open sub-items
- TOML vs YAML final choice; schema-validation + helpful errors for adapter authors.
- The fixed event-block vocabulary's final tag names (`<SPT_EVENT>` etc.).
- Capability-declaration shape beyond `hostable_types` (per-type operation advertisement).
