{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "spt-core runtime manifest",
  "description": "Per-adapter runtime manifest for the spt-core harness contract. Authored as TOML (this schema describes the equivalent data model). A manifest declares only what varies per harness/shell; command templates are opaque strings spt-core never parses. Cross-field invariants (kind<->[shell] agreement, strategy/avenue field requirements) are enforced by spt-core's validate step beyond this schema.",
  "type": "object",
  "properties": {
    "adapter": {
      "$ref": "#/$defs/Adapter"
    },
    "hooks": {
      "description": "`[hooks.<event>]` — inbound hook table, keyed by harness event name.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/Hook"
      }
    },
    "session": {
      "description": "`[session]` — watched-dir keys plus the `[session.<role>]` templates.",
      "$ref": "#/$defs/Session"
    },
    "env": {
      "description": "`[env.<VAR>]` — env-var inject/read table.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/EnvVar"
      }
    },
    "history": {
      "anyOf": [
        {
          "$ref": "#/$defs/History"
        },
        {
          "type": "null"
        }
      ]
    },
    "digest": {
      "description": "`[digest]` — the adapter-declared session-digest extractor seam (ADR-0019).",
      "anyOf": [
        {
          "$ref": "#/$defs/Digest"
        },
        {
          "type": "null"
        }
      ]
    },
    "inject": {
      "anyOf": [
        {
          "$ref": "#/$defs/Inject"
        },
        {
          "type": "null"
        }
      ]
    },
    "message-idle-translation-binary": {
      "description": "`[message-idle-translation-binary]` — opt-in adapter idle-delivery\ntranslation binary (ADR-0022 / REQ-MSG-IDLE-TRANSLATION-BINARY). A TABLE\ncarrying a `path` scalar (modeled as a table, not a bare top-level scalar, so\nan author who writes it after another section cannot have it silently\nabsorbed — and so it stays N+1 extensible). The binary is a pure\nstdin→stdout JSON-lines filter: spt-core feeds it `init`/`event`/`input`\nlines and reads back `{key}`/`{delay_ms}`/`{text}` keystroke-commands, which\nspt-core applies to the broker-held PTY atomically (spt-core owns every PTY\nwrite). spt-core LIFECYCLE-manages it (spawn when the spt-hosted endpoint\ncomes up, terminate when it goes down). A NEW manifest primitive — NOT\ncollapsed into `[inject]`/`notif_command` — though it shares the poll-feed\nsubstrate. Absent ⇒ no translation binary (idle inbound SPOOLS, poll-fed;\nthe v0.11.0 raw-inject was removed as a delivery path, ADR-0022 amendment).",
      "anyOf": [
        {
          "$ref": "#/$defs/IdleTranslationBinary"
        },
        {
          "type": "null"
        }
      ]
    },
    "identity": {
      "anyOf": [
        {
          "$ref": "#/$defs/Identity"
        },
        {
          "type": "null"
        }
      ]
    },
    "update": {
      "anyOf": [
        {
          "$ref": "#/$defs/Update"
        },
        {
          "type": "null"
        }
      ]
    },
    "shell": {
      "description": "`[shell]` body — present iff `adapter.kind = \"shell\"` (validated).",
      "anyOf": [
        {
          "$ref": "#/$defs/Shell"
        },
        {
          "type": "null"
        }
      ]
    },
    "profiles": {
      "description": "`[profiles.<name>]` — **shipped** profile overlays: sparse leaf-replace\noverlays declared by the adapter dev inside the parent manifest, updating\nas one unit with it. Stored raw ([`toml::Value`]); [`crate::profile::resolve`]\nmerges one onto the base and re-validates the complete manifest. A bare\n`adapter_name` ignores these (parent unmodified); the composite\n`<adapter>:<profile>` selects one. **Local** (node-local, user-authored)\nprofiles live beside the adapter in the registry, never here.\n(CONTEXT.md §adapter profile.)",
      "type": "object",
      "additionalProperties": true
    },
    "strings": {
      "description": "`[strings]` — an adapter-authored KV tree of **opaque data** (spt-core\nnever executes a string; command templates live in their own sections\nbehind registration). Dot-path-readable via `spt adapter get-string`, and\nit rides the same leaf-replace profile overlay as the rest of the manifest\n(a shipped or local profile may override base strings). Node-local; no\ncross-node sync. (CONTEXT.md §adapter strings.)",
      "type": "object",
      "additionalProperties": true
    },
    "hints": {
      "description": "`[[hints]]` — once-per-session keyword hints (CONTEXT.md §keyword hints).\n**Order is significant** (first match wins). A profile overlays this by\nleaf-replace like any section — the array is replaced wholesale, never\nspliced (override/extend = re-declare).",
      "type": "array",
      "items": {
        "$ref": "#/$defs/Hint"
      }
    }
  },
  "required": [
    "adapter"
  ],
  "$defs": {
    "Adapter": {
      "description": "`[adapter]` — the manifest header, readable before any update (compat gate).",
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "kind": {
          "$ref": "#/$defs/AdapterKind",
          "default": "harness"
        },
        "version": {
          "type": "string"
        },
        "min_spt_core_version": {
          "description": "Lowest spt-core version this adapter tolerates (compat gate).",
          "type": "string"
        },
        "hostable_types": {
          "description": "Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "shortcut_basename": {
          "description": "Optional basename for the `spt endpoint run` picker's `<basename>-<id>`\nlauncher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default\n`spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts\n(spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted\nfrom serialization when absent). The picker reads it from the RESOLVED\nmanifest of the selected adapter.",
          "type": [
            "string",
            "null"
          ]
        },
        "host_binaries": {
          "description": "The harness executable basenames this `kind=\"harness\"` adapter hosts\nagents inside (e.g. `host_binaries = [\"claude\"]`). The bind-time\nadapter-resolution match-key (REQ-MANIFEST-8 / ADR-0021): a harness\nsession's parent pid → its exe basename selects the candidate adapters\nwhose `host_binaries` contains it (case-insensitive, `.exe`-stripped), so\n`listen`/`poll` resolve the owning adapter at bind with no mandatory\n`--adapter`. Additive + N-1-safe (omitted from serialization when empty,\nlike `shortcut_basename`); an empty list is harmless (the adapter is never\na bind-time candidate, only reachable via the explicit `--adapter`).",
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      },
      "required": [
        "name",
        "version",
        "min_spt_core_version"
      ]
    },
    "AdapterKind": {
      "description": "The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven\nsurface (MANIFEST §Shell adapters).",
      "type": "string",
      "enum": [
        "harness",
        "shell"
      ]
    },
    "Hook": {
      "description": "`[hooks.<event>]` — one harness event → the `api` command it fires, the\nstdin fields it maps, and whether it can surface context to the agent.",
      "type": "object",
      "properties": {
        "fires": {
          "description": "Opaque `api …` command line the harness invokes for this event.",
          "type": "string"
        },
        "reads": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "can_inject": {
          "description": "Whether this hook can inject context (false ⇒ sentinel/relay fallback).",
          "type": "boolean",
          "default": false
        }
      },
      "required": [
        "fires"
      ]
    },
    "Session": {
      "description": "`[session]` — the watched-dir keys (`commune_dir`/`signoff_dir`) co-located\nwith the fixed set of `[session.<role>]` command templates.",
      "type": "object",
      "properties": {
        "commune_dir": {
          "type": [
            "string",
            "null"
          ]
        },
        "signoff_dir": {
          "type": [
            "string",
            "null"
          ]
        },
        "self": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "resume": {
          "description": "`[session.resume]` — the agent's OWN-session NATIVE resume (the `self_`\nsibling, mirroring `psyche_init`→`psyche_resume`). Selected over `self_`\nonly when a bringup carries `--resume <session>` AND this role is declared;\nabsent ⇒ fall back to `[session.self]` (full back-compat). Keys spt-core\nfills are the SAME catalog as `self`: `{id}`, `{session_id}` (the resumed\nid), `{session_name}`, `{adapter_name}` (REQ-SESSION-RESUME-TEMPLATE).",
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "psyche_init": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "psyche_resume": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "echo_commune": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "signoff": {
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        },
        "notif": {
          "description": "`[session.notif]` — the endpoint-native notification render\n(ADR-0007's `notif_command` seam, REQ-NOTIF-2): an OS toast, a\nGameRobot `alert-symbol`, anything the adapter can run. Spawned\ndetached when a notif surfaces at this endpoint, combinable with the\nagent-surface delivery. Keys spt-core fills: `{notif_id}`,\n`{notif_from}`, `{notif_subnet}`, `{notif_body}`.",
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        }
      }
    },
    "SessionRole": {
      "description": "`[session.<role>]` — one opaque outbound command template plus its spawn\ncontext. Model/tools/flags all live inside `command`, never as fields\n(MANIFEST §session roles). No nested tables here (keeps TOML round-trip\nemission scalar-before-table clean).",
      "type": "object",
      "properties": {
        "command": {
          "description": "Opaque command line, with `{key}` substitution placeholders.",
          "type": "string"
        },
        "cwd": {
          "type": [
            "string",
            "null"
          ]
        },
        "recursion_guard_env": {
          "description": "Env var set on summarizer children so their own hooks bail (recursion\nguard).",
          "type": [
            "string",
            "null"
          ]
        },
        "detach": {
          "type": "boolean",
          "default": false
        },
        "env_remove": {
          "description": "Env vars to strip from the child's inherited environment.",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "keys": {
          "description": "Substitution keys spt-core guarantees to fill for this role.",
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      },
      "required": [
        "command"
      ]
    },
    "EnvVar": {
      "description": "`[env.<VAR>]` — a single env-var directive.",
      "type": "object",
      "properties": {
        "direction": {
          "$ref": "#/$defs/EnvDirection"
        },
        "value": {
          "description": "Value to inject (with substitution); required for `inject`.",
          "type": [
            "string",
            "null"
          ]
        },
        "channel": {
          "description": "Harness-hosted injection channel (spt-hosted inherits from the broker).",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "direction"
      ]
    },
    "EnvDirection": {
      "type": "string",
      "enum": [
        "inject",
        "read"
      ]
    },
    "History": {
      "description": "`[history]` — transcript access strategy.",
      "type": "object",
      "properties": {
        "strategy": {
          "$ref": "#/$defs/HistoryStrategy"
        },
        "fetcher": {
          "description": "`fetcher` strategy: adapter binary emitting normalized history.",
          "type": [
            "string",
            "null"
          ]
        },
        "locate_template": {
          "description": "`locate_normalize` strategy: where the raw transcript lives.",
          "type": [
            "string",
            "null"
          ]
        },
        "normalize_command": {
          "description": "`locate_normalize` strategy: command normalizing the raw transcript.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "strategy"
      ]
    },
    "HistoryStrategy": {
      "oneOf": [
        {
          "description": "spt-core asks the adapter (pull, adapter binary emits normalized).",
          "type": "string",
          "const": "fetcher"
        },
        {
          "description": "spt-core locates the raw transcript then normalizes it.",
          "type": "string",
          "const": "locate_normalize"
        },
        {
          "description": "Adapter pushes via `api history-log`; spt-core stores (Path-B).",
          "type": "string",
          "const": "native"
        }
      ]
    },
    "Digest": {
      "description": "`[digest]` — the session-digest extractor seam (ADR-0019). Reverses M9's\n\"no manifest seam\" stance: the digest gets its **own** adapter-declared\nextractor, distinct from `[history]` (which stays opaque + single-session and\nfeeds the echo-commune verbatim). The extractor maps the harness's **native**\nlog → the published `{role, text, tool, ts}` digest-record contract\n([`spt_term::DigestRecord`]).\n\n**Imperative, not a DSL** (ADR-0019 §Decision): real harness logs are nested\n(one line → many entries, mixed block lists, types to filter) — a flat\ndeclarative map cannot express them, and a map powerful enough is a reinvented\nlanguage. So the extractor is an opaque command spt-core never parses, exactly\nlike every other manifest template.\n\n**Source.** By default the extractor reads the **same files as `[history]`**\n(the `locate_template`; DRY). An adapter may override with `source` (the\nown-source escape hatch). `api digest-entry` push remains the always-available\nfallback for a log-less adapter (which declares no `[digest]` at all).\n\n**Presentation.** `window_turns`, `arg_truncation`, and `sprint_collapse` are\nadapter-declared **defaults** any consumer may override at pull/subscribe;\nspt-core ships fallback defaults ([`spt_term::DigestConfig`]) when absent. The\nfixed \"~3 turns\" is no longer an spt-core requirement (ADR-0019).",
      "type": "object",
      "properties": {
        "extractor": {
          "description": "Opaque extractor command: native harness log → the `{role,text,tool,ts}`\ncontract (one JSON record per output line). `{key}` substitution applies\n(e.g. `{session_id}`, and `{source}` for the resolved log path).",
          "type": "string"
        },
        "source": {
          "description": "Own-source escape hatch: a `locate_template` for the log file(s) the\nextractor reads. Absent ⇒ reuse `[history].locate_template` (DRY).",
          "type": [
            "string",
            "null"
          ]
        },
        "window_turns": {
          "description": "Adapter-default window depth (user turns kept). Absent ⇒ spt-core fallback.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 0
        },
        "arg_truncation": {
          "description": "Adapter-default tool-arg truncation width. Absent ⇒ spt-core fallback.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint",
          "minimum": 0
        },
        "sprint_collapse": {
          "description": "Adapter-default for collapsing consecutive tool records into one sprint.\nAbsent ⇒ spt-core fallback (collapse on).",
          "type": [
            "boolean",
            "null"
          ]
        }
      },
      "required": [
        "extractor"
      ]
    },
    "Inject": {
      "description": "`[inject]` — inject-input methods per activity state.",
      "type": "object",
      "properties": {
        "activity": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/InjectMethod"
          }
        },
        "idle": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/InjectMethod"
          }
        }
      }
    },
    "InjectMethod": {
      "type": "string",
      "enum": [
        "pty",
        "hook",
        "relay",
        "http"
      ]
    },
    "IdleTranslationBinary": {
      "description": "`[message-idle-translation-binary]` — the opt-in idle-delivery translation\nbinary (ADR-0022). A table so the contract degrades gracefully: spt-core does\nNOT `deny_unknown_fields`, so a newer adapter declaring a future key (e.g. a\nspawn timeout) against an older spt-core parses fine — the unknown key is\nignored, never a hard manifest failure (a lifecycle-binary contract perri\nbuilds blind from docs must be forward-compatible). Known keys: `command`\n(preferred) and the deprecated `path`.",
      "type": "object",
      "properties": {
        "command": {
          "description": "The opaque command spt-core spawns and lifecycle-manages (ADR-0029):\nprogram + args, with `{adapter_dir}`/`{adapter_name}` substitution; the\nprogram token resolves against `install_dir` like `[digest].extractor` /\n`[session.psyche_init]`. The spawn + stdin/stdout JSON-lines protocol is\nunchanged — `command` only alters how the executable+args are located.\nFolds `claude-spt translate` into the one consolidated adapter binary.",
          "type": [
            "string",
            "null"
          ]
        },
        "path": {
          "description": "**Deprecated** (ADR-0029): the bare binary PATH spt-core spawns. Keeps\nparsing (manifest forward/back-compat) but warns at registration steering\nto `command`. Resolved against `install_dir`; a single program token (no\nsubstitution, never re-tokenized). Exactly one of `{command, path}` —\nboth-set is refused at validation; neither = no translation binary.",
          "type": [
            "string",
            "null"
          ]
        }
      }
    },
    "Identity": {
      "description": "`[identity]` — how the harness's session id is obtained.",
      "type": "object",
      "properties": {
        "session_id_source": {
          "$ref": "#/$defs/SessionIdSource"
        },
        "parent_ancestor_name": {
          "description": "Process-tree anchor name when `session_id` is absent.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "session_id_source"
      ]
    },
    "SessionIdSource": {
      "oneOf": [
        {
          "description": "Discovered after spawn (process-tree / wrapper handoff).",
          "type": "string",
          "const": "post_spawn"
        },
        {
          "description": "Injected as a UUID the harness echoes back.",
          "type": "string",
          "const": "uuid_inject"
        }
      ]
    },
    "Update": {
      "description": "`[update]` — adapter self-update directive (parsed in M2a; conducted in M3).",
      "type": "object",
      "properties": {
        "avenue": {
          "$ref": "#/$defs/UpdateAvenue"
        },
        "command": {
          "description": "`delegated` avenue: the command spt-core delegates to.",
          "type": [
            "string",
            "null"
          ]
        },
        "repo": {
          "description": "`file_pull` / `gh_release` avenue: source repo. For `gh_release` this is\nthe `user/repo` whose **GitHub releases** the adapter ships updates from.",
          "type": [
            "string",
            "null"
          ]
        },
        "path_regex": {
          "description": "`file_pull` avenue: path selector.",
          "type": [
            "string",
            "null"
          ]
        },
        "asset": {
          "description": "`gh_release` avenue: the release **asset** name to fetch (the adapter\n`.spt` archive). Absent ⇒ the default `adapter.spt`, matching the\n`spt adapter add --release` acquisition primitive (REQ-INSTALL-9). Not\napplicable to `delegated` / `file_pull`.",
          "type": [
            "string",
            "null"
          ]
        },
        "signing_key": {
          "description": "The adapter's Ed25519 **content-signing public key** (64 hex chars / 32\nbytes). spt-core verifies a pulled payload against this per-adapter key\nbefore applying it (REQ-UPD-5 adapter content signing, ADR-0004 §D) — the\nadapter author signs their own releases; spt-core's release key stays\nscoped to spt-core. **Required for `file_pull`** (there are bytes to\nverify); **optional for `gh_release`** (absent ⇒ HTTPS+GitHub\nfirst-acquisition trust, the same trust `spt adapter add --release` and\nthe installer first-fetch place; present ⇒ the fetched `.spt` is verified\nfail-closed against this key, REQ-UPD-9); not applicable to `delegated`\n(opaque updater).",
          "type": [
            "string",
            "null"
          ]
        },
        "self_verifies": {
          "description": "`delegated` avenue: the adapter attests its own updater verifies the\ncontent it installs (e.g. `claude.exe plugin update` checks its own\nsignatures). spt-core cannot see a delegated updater's bytes, so it\ndelegates the trust **only** when this is set; an unattested delegated\nupdate is skipped as unverifiable (REQ-UPD-5).",
          "type": "boolean",
          "default": false
        },
        "version_check": {
          "description": "Verify spt-core satisfies `min_spt_core_version` before/after.",
          "type": "boolean",
          "default": false
        },
        "uninstall": {
          "description": "Optional inverse of install — run by `spt adapter remove` once the adapter\nis quiesced (the mirror of `spt adapter add`, which reuses this section as\nthe install mechanism). Absent ⇒ spt-core's default cleanup. (Modeled in\nM2a; conducted with adapter-registration later.)",
          "type": [
            "string",
            "null"
          ]
        },
        "message": {
          "description": "Optional plain (multi-line) human notice surfaced to stdout —\nmarkdown-rendered (the helpfmt prose path) — **only when `spt adapter\nupdate` actually APPLIES an update** (version changed), never on a\nno-op. Read from the newly-installed manifest; avenue-agnostic\n(`gh_release` / `delegated` / `file_pull`). No `{key}` substitution.\nUse: an adapter telling the operator a post-update action (e.g. \"run\n`/reload-plugins` in any ongoing sessions\").",
          "type": [
            "string",
            "null"
          ]
        },
        "post": {
          "description": "`[update.post]` — an **avenue-agnostic** delegated post-step (ADR-0029) run\nAFTER the primary avenue resolves, in the same `spt adapter update`.\nAbsent ⇒ today's behavior exactly (an applied update fires `message`).",
          "anyOf": [
            {
              "$ref": "#/$defs/UpdatePost"
            },
            {
              "type": "null"
            }
          ]
        },
        "transport": {
          "description": "`gh_release` avenue: the fetch **transport** — `https` (direct reqwest,\npublic), `gh` (shell the pre-authorized `gh` CLI, the private-repo path),\nor `auto` (default: prefer `gh` when installed+authed, else HTTPS). `gh`\nhonors OAuth + `GH_TOKEN`, so spt-core custodies no token. Additive over\nthe existing fetch path; not applicable to `delegated` / `file_pull`.\nAbsent ⇒ `auto` (N-1-safe).",
          "$ref": "#/$defs/Transport"
        }
      },
      "required": [
        "avenue"
      ]
    },
    "UpdateAvenue": {
      "oneOf": [
        {
          "description": "Delegate to the adapter's own updater (e.g. `claude plugin update`).",
          "type": "string",
          "const": "delegated"
        },
        {
          "description": "spt-core pulls files from a repo.",
          "type": "string",
          "const": "file_pull"
        },
        {
          "description": "spt-core ships updates from the adapter's own **GitHub releases**\n(REQ-UPD-9): compare the repo's latest release version against the\ninstalled adapter version and, when newer, fetch the release `.spt`\narchive (the REQ-INSTALL-9 `--release` primitive), verify it against an\noptional `signing_key` (else HTTPS+GitHub trust), and re-register. No\nsigning tooling or plugin coupling required of the adapter author.",
          "type": "string",
          "const": "gh_release"
        }
      ]
    },
    "UpdatePost": {
      "description": "`[update.post]` — an avenue-agnostic delegated post-step (ADR-0029) run after\nthe primary `[update]` avenue (gh_release / file_pull / delegated) resolves,\nin the same `spt adapter update`. Runs **unconditionally** — even when the\nadapter pull was a version no-op — because the post-step does its own\nidempotent check (e.g. `claude plugin update`). spt-core feeds it the update\noutcome as one stdin JSON line (`adapter_applied`, `adapter_name`,\n`profile_name`, `version`, `previous_version`, `adapter_dir`; additive keys)\nand reads its stdout to arbitrate the post-update notice (custom text\nsupersedes `[update].message`; the reserved sentinel fires the static\n`[update].message`; empty = no notice). exit code is orthogonal (0 ok /\nnonzero failed). Failure-isolated: a committed `gh_release` pull is never\nrolled back if the post-step fails. (A table so unknown future keys degrade\ngracefully — no `deny_unknown_fields`.)",
      "type": "object",
      "properties": {
        "command": {
          "description": "The command spt-core runs after pull+re-register. Opaque; `{adapter_dir}`/\n`{adapter_name}` substitution; the program token resolves against the\ninstall dir (REQ-INSTALL-11). Validated non-empty.",
          "type": "string"
        },
        "self_verifies": {
          "description": "The post-step attests it verifies the content it installs (mirrors\n`[update].self_verifies` for the delegated avenue — e.g. `claude plugin\nupdate` checks its own signatures). Attestation metadata; the post-step\nruns unconditionally regardless (the trust model around delegated content\nthe post-step installs, not an execution gate).",
          "type": "boolean",
          "default": false
        }
      },
      "required": [
        "command"
      ]
    },
    "Transport": {
      "description": "Fetch transport for the `gh_release` avenue (and `spt adapter add --release`):\nhow spt-core retrieves the release asset bytes + the latest-release version.\n(REQ-ADAPTER-GH-TRANSPORT)",
      "oneOf": [
        {
          "description": "Direct HTTPS via reqwest — the public-repo path (the original behavior).",
          "type": "string",
          "const": "https"
        },
        {
          "description": "Shell the pre-authorized `gh` CLI (`gh release download` for the asset,\n`gh api` for the version) — the private-repo path. `gh` honors OAuth +\n`GH_TOKEN`, so spt-core never custodies a token.",
          "type": "string",
          "const": "gh"
        },
        {
          "description": "Prefer `gh` when it is installed and authenticated, else fall back to\nHTTPS. The default.",
          "type": "string",
          "const": "auto"
        }
      ]
    },
    "Shell": {
      "description": "`[shell]` — the body of a `kind = \"shell\"` adapter (a driven surface).",
      "type": "object",
      "properties": {
        "spawn": {
          "description": "Broker-launched opaque spawn command.",
          "type": "string"
        },
        "ephemeral": {
          "description": "Ephemeral ⇒ no offline perch + no history retention.",
          "type": "boolean",
          "default": false
        },
        "broadcast": {
          "anyOf": [
            {
              "$ref": "#/$defs/Broadcast"
            },
            {
              "type": "null"
            }
          ]
        },
        "command_receipt": {
          "description": "How the shell receives agent commands.",
          "anyOf": [
            {
              "$ref": "#/$defs/CommandReceipt"
            },
            {
              "type": "null"
            }
          ]
        },
        "pre_close": {
          "description": "Instruction sent to the binary on link-break.",
          "type": [
            "string",
            "null"
          ]
        },
        "close_timeout_ms": {
          "description": "Graceful-termination window before force-close.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint64",
          "minimum": 0
        },
        "persistent": {
          "description": "Auto-online whenever the owner endpoint is online.",
          "type": "boolean",
          "default": false
        },
        "wake_command": {
          "description": "Long-running wake-watcher run WHILE offline; exit ⇒ revive.",
          "type": [
            "string",
            "null"
          ]
        },
        "can_shutdown": {
          "description": "Whether the shell may fire `api owner-shutdown` to suspend its owner.",
          "type": "boolean",
          "default": false
        },
        "require_approval": {
          "description": "Per-spawn user approval gate (floor; a node/endpoint setting may tighten).\nAbsent ⇒ `none`. (Modeled now; conducted when shells land.)",
          "$ref": "#/$defs/ShellApproval"
        },
        "max_instances_per_owner": {
          "description": "Optional ceiling on concurrent-existing instances per owner endpoint\n(online + offline both count). Absent ⇒ unlimited.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint32",
          "minimum": 0
        },
        "over_cap": {
          "description": "What happens at the cap: `reject` (default) or `approve` (per-spawn\napproval beyond the cap; does not raise it). Only meaningful with a cap.",
          "$ref": "#/$defs/OverCap"
        },
        "capabilities": {
          "description": "`[shell.capabilities]` — the command vocabulary (agent→shell).",
          "type": "object",
          "additionalProperties": {
            "$ref": "#/$defs/ShellCapability"
          }
        },
        "sensory": {
          "description": "`[shell.sensory]` — the sensory vocabulary (shell→agent).",
          "$ref": "#/$defs/Sensory"
        },
        "drive": {
          "description": "`[shell.drive]` — the drive vocabulary (agent→shell, REST-only ephemeral\ncontrol; M11-W2, REQ-SHELL-3). The owner→shell mirror of `[shell.sensory]`.",
          "$ref": "#/$defs/Drive"
        },
        "tunnel": {
          "description": "`[shell.tunnel]` — opt-in for the opaque reliable-ordered byte tunnel\n(M11-W3, REQ-SHELL-4). Absent ⇒ no tunnel.",
          "$ref": "#/$defs/Tunnel"
        }
      },
      "required": [
        "spawn"
      ]
    },
    "Broadcast": {
      "type": "string",
      "enum": [
        "subnet",
        "same-node",
        "none"
      ]
    },
    "CommandReceipt": {
      "type": "string",
      "enum": [
        "http",
        "stdin",
        "relay"
      ]
    },
    "ShellApproval": {
      "description": "Per-shell instantiation-approval mode (`require_approval`). Reuses the consent\nplumbing: `remembered` lets allow-always write a persistent grant; `always`\nsuppresses allow-always (prompt every spawn).",
      "oneOf": [
        {
          "description": "No approval (default — matches the system's everything-opt-in posture).",
          "type": "string",
          "const": "none"
        },
        {
          "description": "Prompt; allow-always persists a grant, later spawns auto-allow.",
          "type": "string",
          "const": "remembered"
        },
        {
          "description": "Prompt on every spawn; allow-always suppressed (no persistent grant).",
          "type": "string",
          "const": "always"
        }
      ]
    },
    "OverCap": {
      "description": "What happens when an owner is at its `max_instances_per_owner` cap.",
      "oneOf": [
        {
          "description": "Refuse the spawn outright (default).",
          "type": "string",
          "const": "reject"
        },
        {
          "description": "Require per-spawn approval beyond the cap (does not raise the cap).",
          "type": "string",
          "const": "approve"
        }
      ]
    },
    "ShellCapability": {
      "description": "One entry in `[shell.capabilities]` — a command and its argument names.\n\nA capability may carry its own **act-gate** (M11, REQ-CONSENT-3): the same\n`require_approval` enum the spawn gate uses, riding the individual op so the\n*dangerous act* is gated, not just the spawn. An optional `class_key` scopes\nthe written grant finer than the op id — granted per `(owner × class × node)`\n(the usbip `attach`'s device class), so a remembered HID-class grant never\nauthorizes a storage-class act. Spawn gates govern existence; capability\ngates govern acts (CONTEXT §\"per-capability approval gates\").",
      "type": "object",
      "properties": {
        "args": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "require_approval": {
          "description": "Per-act approval gate (floor; a node/endpoint setting may tighten).\nAbsent ⇒ `none` (ungated).",
          "$ref": "#/$defs/ShellApproval"
        },
        "class_key": {
          "description": "Optional grant-qualifier class so a grant scopes finer than the op id\n(`(owner × class × node)`). Only meaningful with a gated `require_approval`.",
          "type": [
            "string",
            "null"
          ]
        }
      }
    },
    "Sensory": {
      "description": "`[shell.sensory]` — the sensory payload types a shell may emit.",
      "type": "object",
      "properties": {
        "types": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "Drive": {
      "description": "`[shell.drive]` — the drive payload types an agent may push to a shell\n(M11-W2, REQ-SHELL-3). The owner→shell mirror of [`Sensory`]: REST-only,\nephemeral latest-wins, never spooled.",
      "type": "object",
      "properties": {
        "types": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "Tunnel": {
      "description": "`[shell.tunnel]` — the opt-in for the opaque reliable-ordered byte tunnel\n(M11-W3, REQ-SHELL-4): a dedicated QUIC stream pair bound to the owner↔shell\nlink, carrying wire protocol traffic the channel taxonomy must NOT reinterpret\n(first consumer: USB/IP URB traffic). Not enveloped, not MAC-framed, not\nspooled; the link lifecycle governs it (a link-break closes the tunnel).\nReliable-ordered ⇒ congestion surfaces as lag never loss ⇒ on-LAN posture.",
      "type": "object",
      "properties": {
        "enable": {
          "description": "Whether this shell opens the opaque tunnel on link-up.",
          "type": "boolean",
          "default": false
        },
        "protocol": {
          "description": "Optional diagnostic label for the opaque wire protocol (e.g. `usbip-urb`).\nInformational only — the substrate never interprets tunnel bytes.",
          "type": [
            "string",
            "null"
          ]
        }
      }
    },
    "Hint": {
      "description": "`[[hints]]` — one once-per-session keyword hint (CONTEXT.md §keyword hints).\nThe adapter's user-prompt hook pipes the full user message to `spt api hint`;\na matching keyword surfaces `text` to the agent's context channel, at most\nonce per session and once per message.",
      "type": "object",
      "properties": {
        "keywords": {
          "description": "Keywords that fire the hint — literal **case-insensitive substrings** by\ndefault; compiled as **regex** patterns when `regex = true`.",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "text": {
          "description": "The hint text surfaced when a keyword matches.",
          "type": "string"
        },
        "regex": {
          "description": "Treat `keywords` as regex patterns instead of literal substrings.",
          "type": "boolean",
          "default": false
        }
      },
      "required": [
        "text"
      ]
    }
  },
  "$id": "https://sabermage.github.io/spt-releases/manifest.schema.json"
}
