traceable-reqs.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) test manifest::tests::digest_section_validation_errors ... ok test manifest::tests::checked_in_schema_is_current ... FAILED test manifest::tests::syntactically_broken_toml_is_an_error_not_a_panic ... ok test profile::tests::resolve_unknown_profile_errors ... ok test manifest::tests::unknown_enum_variant_is_an_error_not_a_panic ... ok test resolve::tests::resolve_error_hints_print_the_group_level_adapter_form ... ok STRINGS_FILE_SKIP: cannot read string file C:\Users\decid\AppData\Local\Temp\.tmpbybqAZ\adapters\mock-cc\strings\skill.md (The system cannot find the file specified. (os error 2)) test registry::tests::missing_string_file_skips_at_read_without_error ... ok test runtime::tests::missing_key_and_empty_command_errors_still_fire ... ok test runtime::tests::missing_key_errors_before_spawn ... ok test runtime::tests::unknown_role_errors ... ok thread 'manifest::tests::checked_in_schema_is_current' (53004) panicked at crates\spt-runtime\src\manifest.rs:2797:9: assertion `left == right` failed: manifest.schema.json drifted from the derives — regenerate with SPT_BLESS=1 cargo test -p spt-runtime left: "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"spt-core runtime manifest\",\n \"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.\",\n \"type\": \"object\",\n \"properties\": {\n \"adapter\": {\n \"$ref\": \"#/$defs/Adapter\"\n },\n \"hooks\": {\n \"description\": \"`[hooks.]` — inbound hook table, keyed by harness event name.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/$defs/Hook\"\n }\n },\n \"session\": {\n \"description\": \"`[session]` — watched-dir keys plus the `[session.]` templates.\",\n \"$ref\": \"#/$defs/Session\"\n },\n \"env\": {\n \"description\": \"`[env.]` — env-var inject/read table.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/$defs/EnvVar\"\n }\n },\n \"history\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/History\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"digest\": {\n \"description\": \"`[digest]` — the adapter-declared session-digest extractor seam.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Digest\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"inject\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Inject\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"io\": {\n \"description\": \"`[io]` — the adapter's IO-funnel compliance declaration. Absent ⇒ core\\nparses NOTHING of this adapter's ingest, which makes migration safe.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Io\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"message-idle-translation-binary\": {\n \"description\": \"`[message-idle-translation-binary]` — opt-in adapter idle-delivery\\ntranslation binary. A TABLE carrying a `path` scalar (modeled as a table,\\nnot 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 delivery path has been removed).\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/IdleTranslationBinary\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"service\": {\n \"description\": \"`[service]` — the daemon-supervised ResidentService this adapter owns.\\nAbsent ⇒ the adapter has no supervised binary. Independent of\\n`adapter.kind`: a shell adapter's service is the shape the first consumer\\nships, but nothing about the substrate requires a shell.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Service\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"identity\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Identity\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"update\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Update\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"shell\": {\n \"description\": \"`[shell]` body — present iff `adapter.kind = \\\"shell\\\"` (validated).\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Shell\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"profiles\": {\n \"description\": \"`[profiles.]` — **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`:` selects one. **Local** (node-local, user-authored)\\nprofiles live beside the adapter in the registry, never here.\\n(CONTEXT.md §adapter profile.)\",\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"strings\": {\n \"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.)\",\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"hints\": {\n \"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).\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/Hint\"\n }\n }\n },\n \"required\": [\n \"adapter\"\n ],\n \"$defs\": {\n \"Adapter\": {\n \"description\": \"`[adapter]` — the manifest header, readable before any update (compat gate).\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"kind\": {\n \"$ref\": \"#/$defs/AdapterKind\",\n \"default\": \"harness\"\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"min_spt_core_version\": {\n \"description\": \"Lowest spt-core version this adapter tolerates (compat gate).\",\n \"type\": \"string\"\n },\n \"hostable_types\": {\n \"description\": \"Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"shortcut_basename\": {\n \"description\": \"Optional basename for the `spt endpoint run` picker's `-`\\nlauncher shortcut. Absent ⇒ the harness-agnostic default\\n`spt` (→ `spt-`); an adapter sets this to brand its shortcuts\\n(spt-claude-code → `cc`, giving `cc-`). Additive + N-1-safe (omitted\\nfrom serialization when absent). The picker reads it from the RESOLVED\\nmanifest of the selected adapter.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"web_short_path\": {\n \"description\": \"Optional served-name alias for the base adapter's core-owned `web/` root.\\nThe adapter facet exists even when this field is absent.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"docs_dir\": {\n \"description\": \"Optional ADAPTER-RELATIVE directory holding the adapter's own docs,\\nserved under the reserved `docs` segment of its adapter facet\\n(`//a//docs/…`). Absent ⇒ that segment answers 404 naming\\nthe facet; it never falls through to the core-owned `web/` root, which\\nkeeps the meaning of an already-shipped URL fixed. Absolute paths and\\nparent-directory escapes are refused by name at load AND re-resolved at\\nrequest time.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"host_binaries\": {\n \"description\": \"The harness executable basenames this `kind=\\\"harness\\\"` adapter hosts\\nagents inside (e.g. `host_binaries = [\\\"claude\\\"]`). This is the bind-time\\nadapter-resolution match-key: a harness session's parent pid → its exe\\nbasename 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`).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"name\",\n \"version\",\n \"min_spt_core_version\"\n ]\n },\n \"AdapterKind\": {\n \"description\": \"The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven\\nsurface (MANIFEST §Shell adapters).\",\n \"type\": \"string\",\n \"enum\": [\n \"harness\",\n \"shell\"\n ]\n },\n \"Hook\": {\n \"description\": \"`[hooks.]` — one harness event → the `api` command it fires, the\\nstdin fields it maps, and whether it can surface context to the agent.\",\n \"type\": \"object\",\n \"properties\": {\n \"fires\": {\n \"description\": \"Opaque `api …` command line the harness invokes for this event.\",\n \"type\": \"string\"\n },\n \"reads\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"can_inject\": {\n \"description\": \"Whether this hook can inject context (false ⇒ sentinel/relay fallback).\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\n \"fires\"\n ]\n },\n \"Session\": {\n \"description\": \"`[session]` — the watched-dir keys (`commune_dir`/`signoff_dir`) co-located\\nwith the fixed set of `[session.]` command templates.\",\n \"type\": \"object\",\n \"properties\": {\n \"commune_dir\": {\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"signoff_dir\": {\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"self\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/SessionRole\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"resume\": {\n \"description\": \"`[session.resume]` — the agent's OWN-session NATIVE resume (the `self_`\\nsibling, mirroring `psyche_init`→`psyche_resume`). Selected over `self_`\\nwhen a bringup carries RESUME INTENT and this role is declared; absent ⇒\\nfall back to `[session.self]` (full back-compat, and a resume then starts\\na blank transcript). The resume verbs are `spt endpoint resume `,\\n`spt go ` on an offline endpoint, and the picker's *Resume from\\nhistory*. Resume is LATEST-ONLY: no verb takes a session argument, and one\\nis refused rather than ignored. Keys spt-core fills are the SAME catalog\\nas `self`: `{id}`, `{session_id}` (the resumed id), `{session_name}`,\\n`{adapter_name}`.\\n\\n`{session_id}` is always a HARNESS-REPORTED session id read from the\\nendpoint's ledger — never spt-core's own spawn-time provisional, which the\\nharness has never seen and could not resolve. When the only thing on\\nrecord is such a provisional, spt-core does NOT feed it to this template:\\nit prints `RESUME_NO_HARNESS_SESSION:` and starts a FRESH session\\nthrough `[session.self]` instead. That is the third outcome of a resume,\\nit is loud by design, and a declared `[session.resume]` will simply not\\nrun for it.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/SessionRole\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"psyche_init\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/SessionRole\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"psyche_resume\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/SessionRole\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"echo_commune\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/SessionRole\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"signoff\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/SessionRole\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"notif\": {\n \"description\": \"`[session.notif]` — the endpoint-native notification render: an OS toast,\\na GameRobot `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}`.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/SessionRole\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n }\n },\n \"SessionRole\": {\n \"description\": \"`[session.]` — 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).\",\n \"type\": \"object\",\n \"properties\": {\n \"command\": {\n \"description\": \"Opaque command line, with `{key}` substitution placeholders.\",\n \"type\": \"string\"\n },\n \"cwd\": {\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"recursion_guard_env\": {\n \"description\": \"Env var stamped (`=1`) on this role's spawned process so the child's own\\nhooks bail instead of recursing (recursion guard). Honored for ANY role\\nthat declares the field — core keys on the field, never on a role name.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"detach\": {\n \"type\": \"boolean\",\n \"default\": false\n },\n \"env_remove\": {\n \"description\": \"Env vars to strip from the child's inherited environment.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"keys\": {\n \"description\": \"Substitution keys spt-core guarantees to fill for this role.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"invocation_budget_secs\": {\n \"description\": \"How long a BOUNDED invocation of this role may run before spt-core kills it,\\nin seconds. The adapter declares it because the adapter is the only party that\\nknows what its own model costs; spt-core clamps it so a declaration cannot buy\\nan unbounded spawn.\\n\\nAbsent ⇒ [`DEFAULT_INVOCATION_BUDGET_SECS`]. Above the ceiling ⇒ clamped to\\n[`MAX_INVOCATION_BUDGET_SECS`]. Read through [`invocation_budget`] at every\\nbounded spawn — the resolution keys on THIS FIELD, never on which role it is\\n(the same discipline `recursion_guard_env` carries).\",\n \"type\": [\n \"integer\",\n \"null\"\n ],\n \"format\": \"uint64\",\n \"minimum\": 0\n }\n },\n \"required\": [\n \"command\"\n ]\n },\n \"EnvVar\": {\n \"description\": \"`[env.]` — a single env-var directive.\",\n \"type\": \"object\",\n \"properties\": {\n \"direction\": {\n \"$ref\": \"#/$defs/EnvDirection\"\n },\n \"value\": {\n \"description\": \"Value to inject (with substitution); required for `inject`.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"channel\": {\n \"description\": \"Harness-hosted injection channel (spt-hosted inherits from the broker).\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n }\n },\n \"required\": [\n \"direction\"\n ]\n },\n \"EnvDirection\": {\n \"type\": \"string\",\n \"enum\": [\n \"inject\",\n \"read\"\n ]\n },\n \"History\": {\n \"description\": \"`[history]` — transcript access strategy.\",\n \"type\": \"object\",\n \"properties\": {\n \"strategy\": {\n \"$ref\": \"#/$defs/HistoryStrategy\"\n },\n \"fetcher\": {\n \"description\": \"`fetcher` strategy: adapter binary emitting normalized history.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"locate_template\": {\n \"description\": \"`locate_normalize` strategy: where the raw transcript lives.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"normalize_command\": {\n \"description\": \"`locate_normalize` strategy: command normalizing the raw transcript.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n }\n },\n \"required\": [\n \"strategy\"\n ]\n },\n \"HistoryStrategy\": {\n \"oneOf\": [\n {\n \"description\": \"spt-core asks the adapter (pull, adapter binary emits normalized).\",\n \"type\": \"string\",\n \"const\": \"fetcher\"\n },\n {\n \"description\": \"spt-core locates the raw transcript then normalizes it.\",\n \"type\": \"string\",\n \"const\": \"locate_normalize\"\n },\n {\n \"description\": \"Adapter pushes via `api history-log`; spt-core stores (Path-B).\",\n \"type\": \"string\",\n \"const\": \"native\"\n }\n ]\n },\n \"Digest\": {\n \"description\": \"`[digest]` — the session-digest extractor seam. Reverses the prior \\\"no\\nmanifest 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**: 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.\",\n \"type\": \"object\",\n \"properties\": {\n \"strategy\": {\n \"description\": \"Which side locates the transcript. Absent ⇒\\n`locate_normalize` (spt-core locates + pre-reads `source`) — the original\\nbehavior, so every pre-strategy manifest parses unchanged.\",\n \"$ref\": \"#/$defs/DigestStrategy\",\n \"default\": \"locate_normalize\"\n },\n \"extractor\": {\n \"description\": \"Opaque extractor command: native harness log → the `{role,text,tool,ts}`\\ncontract (one JSON record per output line). `{key}` substitution applies\\n(`{session_id}`, the perch-bound `{cwd}`, captured `[env] direction=\\\"read\\\"`\\nvars, and — in `locate_normalize` mode — `{source}` for the resolved path).\",\n \"type\": \"string\"\n },\n \"source\": {\n \"description\": \"Own-source escape hatch (`locate_normalize` only): a `locate_template` for\\nthe log file the extractor reads. Absent ⇒ reuse `[history].locate_template`\\n(DRY). Ignored under `fetcher` (the extractor locates itself).\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"window_turns\": {\n \"description\": \"Adapter-default window depth (user turns kept). Absent ⇒ spt-core fallback.\",\n \"type\": [\n \"integer\",\n \"null\"\n ],\n \"format\": \"uint\",\n \"minimum\": 0\n },\n \"arg_truncation\": {\n \"description\": \"Adapter-default tool-arg truncation width. Absent ⇒ spt-core fallback.\",\n \"type\": [\n \"integer\",\n \"null\"\n ],\n \"format\": \"uint\",\n \"minimum\": 0\n },\n \"sprint_collapse\": {\n \"description\": \"Adapter-default for collapsing consecutive tool records into one sprint.\\nAbsent ⇒ spt-core fallback (collapse on).\",\n \"type\": [\n \"boolean\",\n \"null\"\n ]\n }\n },\n \"required\": [\n \"extractor\"\n ]\n },\n \"DigestStrategy\": {\n \"description\": \"`[digest]` locate-ownership strategy — mirrors [`HistoryStrategy`]'s\\nlocate/normalize split, applied to the digest extractor.\\nWhich side LOCATES the transcript:\\n- `locate_normalize` (default, back-compat): **spt-core** resolves the single\\n `source` template to one file, reads it, and pipes the bytes to the extractor\\n on stdin. Correct for a trivial single-file, fully-templatable layout.\\n- `fetcher`: the **adapter's** extractor locates + reads + emits normalized\\n records itself; spt-core only runs it bounded and consumes stdout (no locate,\\n no pre-read). Required for a PARTITIONED layout spt-core cannot express as one\\n template — e.g. Claude Code's `projects//.jsonl` or a\\n date-globbed rollout tree — the CONTEXT §history \\\"the adapter owns the locate\\\"\\n case. spt-core feeds the extractor only the harness-NEUTRAL inputs it owns\\n (`{session_id}`, the perch-bound `{cwd}`, and captured `[env] direction=\\\"read\\\"`\\n vars), never a harness-specific project slug.\",\n \"oneOf\": [\n {\n \"description\": \"spt-core locates the single `source` file and pipes its bytes to the\\nextractor (the original behavior and the default for back-compat).\",\n \"type\": \"string\",\n \"const\": \"locate_normalize\"\n },\n {\n \"description\": \"The adapter's extractor locates + reads + emits; spt-core runs it bounded\\nand consumes stdout (no `source`, no pre-read).\",\n \"type\": \"string\",\n \"const\": \"fetcher\"\n }\n ]\n },\n \"Inject\": {\n \"description\": \"`[inject]` — inject-input methods per activity state.\",\n \"type\": \"object\",\n \"properties\": {\n \"activity\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/InjectMethod\"\n }\n },\n \"idle\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/InjectMethod\"\n }\n }\n }\n },\n \"InjectMethod\": {\n \"type\": \"string\",\n \"enum\": [\n \"pty\",\n \"hook\",\n \"relay\",\n \"http\"\n ]\n },\n \"Io\": {\n \"description\": \"`[io]` — the adapter's IO-funnel declaration.\\n\\n**The default is OFF, and that is the whole point.** Operator ruling 9\\nenables core-side shortform per-adapter only on an explicit compliance\\ndeclaration, because an adapter shipping its own parser must remain the ONLY\\nparser until its own release deletes that parser and declares here, in one\\nchange. A default-on core would double-fire against every adapter already in\\nthe field the moment core upgraded — the exact window the ruling forbids.\\n\\n`shortform` is ruling 8's opt-out for an exotic harness whose output cannot\\nlive with the grammar: absent means enabled (declaring compliance is a\\nstatement that the adapter is ready), `false` disables just this reader while\\nleaving the declaration standing. It is ONE switch over BOTH shortform\\nmarkers — the `@<…@>` dispatch tag and the `;;` seal mint — because ruling 8\\nmakes them one feature with one suppression grammar, and a knob each would\\nbe two answers to the question \\\"does core parse my output\\\".\",\n \"type\": \"object\",\n \"properties\": {\n \"compliance\": {\n \"description\": \"The adapter declares it is IO-funnel compliant: its own local parsers are\\ngone as of this release, so core may parse its ingest.\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"shortform\": {\n \"description\": \"Opt out of core-side shortform parsing — BOTH the `@<…@>` dispatch tag\\nand the `;;` seal mint — while staying compliant. Absent ⇒ enabled.\",\n \"type\": [\n \"boolean\",\n \"null\"\n ]\n },\n \"now_signal\": {\n \"description\": \"`[io.now_signal]` — standing now-signal tuning for this adapter.\\nAbsent ⇒ the default picture.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/NowSignalSpec\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n }\n },\n \"NowSignalSpec\": {\n \"description\": \"`[io.now_signal]` — which now-signal categories this adapter wants, and how\\nmuch of each (ratified 2026-07-29).\\n\\n**It narrows and tunes; it never invents.** Names outside the closed v1\\nvocabulary are ignored by the reader rather than refused here: an adapter may\\nlegitimately carry a deferred category's name ahead of core building it, and\\na manifest that will not LOAD over a forward-looking category is a manifest\\nthat makes the deferral cost the adapter something.\",\n \"type\": \"object\",\n \"properties\": {\n \"only\": {\n \"description\": \"When non-empty, the ONLY categories to render.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"without\": {\n \"description\": \"Categories to suppress.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"max_lines\": {\n \"description\": \"A per-poll cap on lines per category.\",\n \"type\": [\n \"integer\",\n \"null\"\n ],\n \"format\": \"uint\",\n \"minimum\": 0\n }\n }\n },\n \"IdleTranslationBinary\": {\n \"description\": \"`[message-idle-translation-binary]` — the opt-in idle-delivery translation\\nbinary. 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`.\",\n \"type\": \"object\",\n \"properties\": {\n \"command\": {\n \"description\": \"The opaque command spt-core spawns and lifecycle-manages: program + args,\\nwith `{adapter_dir}`/`{adapter_name}` substitution; the\\nprogram token resolves against `install_dir` like `[digest].extractor` /\\n`[session.psyche_resume]`. 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.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"path\": {\n \"description\": \"**Deprecated**: 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.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n }\n }\n },\n \"Service\": {\n \"description\": \"`[service]` — a **ResidentService**: a binary the DAEMON supervises on the\\nadapter's behalf. Core-owned from birth — spawned job-neutrally by the daemon,\\nso it is never a shell's child (`/T` tree-kill cannot reach\\nit) and never inside a launching terminal's Job Object. It has no perch, no\\nidentity, and no address; a service needing a two-way agent-facing surface\\nhas one at its adapter's endpoint/shell layer.\\n\\nA table, and deliberately NOT `deny_unknown_fields` (the\\n[`IdleTranslationBinary`] reasoning): a newer adapter declaring a future key\\nagainst an older spt-core parses fine rather than hard-failing an adapter\\nwhose author built blind from the published docs.\",\n \"type\": \"object\",\n \"properties\": {\n \"command\": {\n \"description\": \"The opaque command the daemon spawns: program + args, with\\n`{adapter_dir}`/`{adapter_name}` substitution, the program token\\nresolving against `install_dir` before `PATH` exactly as\\n`[digest].extractor` and `[message-idle-translation-binary].command` do,\\nusing one resolution primitive with no parallel path.\",\n \"type\": \"string\"\n },\n \"start\": {\n \"description\": \"When the supervisor reconciles this service toward running. REQUIRED —\\nthere is deliberately no default: the choice decides whether a\\nleast-trusted third-party binary rises with the daemon itself, and an\\nadapter author must say so rather than inherit it.\",\n \"$ref\": \"#/$defs/ServiceStart\"\n },\n \"stop_grace_ms\": {\n \"description\": \"Cooperative-exit grace, in milliseconds, before the supervisor stops\\nwaiting and force-kills (quiesce is exit-when-safe + deadline: delay is\\npossible, veto is not).\",\n \"type\": \"integer\",\n \"format\": \"uint64\",\n \"minimum\": 0,\n \"default\": 30000\n }\n },\n \"required\": [\n \"command\",\n \"start\"\n ]\n },\n \"ServiceStart\": {\n \"description\": \"`[service].start` — the reconcile trigger.\",\n \"oneOf\": [\n {\n \"description\": \"**Desired-state-running**, not an event: the supervisor reconciles\\ntoward running at daemon boot, at adapter registration against a live\\ndaemon (installing an adapter never requires restarting spt to bring its\\nservice up), at update-hold release, and at first shell bind as the\\ndefensive ensure.\",\n \"type\": \"string\",\n \"const\": \"boot\"\n },\n {\n \"description\": \"Lazy: starts at the adapter's first shell bind. Supervised identically\\nonce running.\",\n \"type\": \"string\",\n \"const\": \"bind\"\n }\n ]\n },\n \"Identity\": {\n \"description\": \"`[identity]` — how the harness's session id is obtained.\",\n \"type\": \"object\",\n \"properties\": {\n \"session_id_source\": {\n \"$ref\": \"#/$defs/SessionIdSource\"\n },\n \"parent_ancestor_name\": {\n \"description\": \"Process-tree anchor name when `session_id` is absent.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n }\n },\n \"required\": [\n \"session_id_source\"\n ]\n },\n \"SessionIdSource\": {\n \"oneOf\": [\n {\n \"description\": \"Discovered after spawn (process-tree / wrapper handoff).\",\n \"type\": \"string\",\n \"const\": \"post_spawn\"\n },\n {\n \"description\": \"Injected as a UUID the harness echoes back.\",\n \"type\": \"string\",\n \"const\": \"uuid_inject\"\n }\n ]\n },\n \"Update\": {\n \"description\": \"`[update]` — adapter self-update directive (parsed in M2a; conducted in M3).\",\n \"type\": \"object\",\n \"properties\": {\n \"avenue\": {\n \"$ref\": \"#/$defs/UpdateAvenue\"\n },\n \"command\": {\n \"description\": \"`delegated` avenue: the command spt-core delegates to.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"repo\": {\n \"description\": \"`file_pull` / `gh_release` avenue: source repo. For `gh_release` this is\\nthe `user/repo` whose **GitHub releases** the adapter ships updates from.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"path_regex\": {\n \"description\": \"`file_pull` avenue: path selector.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"asset\": {\n \"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. Not applicable to\\n`delegated` / `file_pull`.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"signing_key\": {\n \"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 — the adapter author signs their own releases;\\nspt-core's release key stays scoped to spt-core. **Required for\\n`file_pull`** (there are bytes to verify); **optional for `gh_release`**\\n(absent ⇒ HTTPS+GitHub first-acquisition trust, the same trust\\n`spt adapter add --release` and the installer first-fetch place; present\\n⇒ the fetched `.spt` is verified fail-closed against this key); not\\napplicable to `delegated` (opaque updater).\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"self_verifies\": {\n \"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.\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"uninstall\": {\n \"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.)\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"message\": {\n \"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\\\").\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"post\": {\n \"description\": \"`[update.post]` — an **avenue-agnostic** delegated post-step run\\nAFTER the primary avenue resolves, in the same `spt adapter update`.\\nAbsent ⇒ today's behavior exactly (an applied update fires `message`).\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/UpdatePost\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"transport\": {\n \"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).\",\n \"$ref\": \"#/$defs/Transport\"\n }\n },\n \"required\": [\n \"avenue\"\n ]\n },\n \"UpdateAvenue\": {\n \"oneOf\": [\n {\n \"description\": \"Delegate to the adapter's own updater (e.g. `claude plugin update`).\",\n \"type\": \"string\",\n \"const\": \"delegated\"\n },\n {\n \"description\": \"spt-core pulls files from a repo.\",\n \"type\": \"string\",\n \"const\": \"file_pull\"\n },\n {\n \"description\": \"spt-core ships updates from the adapter's own **GitHub releases**:\\ncompare the repo's latest release version against the installed adapter\\nversion and, when newer, fetch the release `.spt` archive using the same\\nprimitive as `spt adapter add --release`, verify it against an optional\\n`signing_key` (else HTTPS+GitHub trust), and re-register. No\\nsigning tooling or plugin coupling required of the adapter author.\",\n \"type\": \"string\",\n \"const\": \"gh_release\"\n }\n ]\n },\n \"UpdatePost\": {\n \"description\": \"`[update.post]` — an avenue-agnostic delegated post-step run after the\\nprimary `[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`.)\",\n \"type\": \"object\",\n \"properties\": {\n \"command\": {\n \"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. Validated non-empty.\",\n \"type\": \"string\"\n },\n \"self_verifies\": {\n \"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).\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\n \"command\"\n ]\n },\n \"Transport\": {\n \"description\": \"Fetch transport for the `gh_release` avenue (and `spt adapter add --release`):\\nhow spt-core retrieves the release asset bytes and latest-release version.\",\n \"oneOf\": [\n {\n \"description\": \"Direct HTTPS via reqwest — the public-repo path (the original behavior).\",\n \"type\": \"string\",\n \"const\": \"https\"\n },\n {\n \"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.\",\n \"type\": \"string\",\n \"const\": \"gh\"\n },\n {\n \"description\": \"Prefer `gh` when it is installed and authenticated, else fall back to\\nHTTPS. The default.\",\n \"type\": \"string\",\n \"const\": \"auto\"\n }\n ]\n },\n \"Shell\": {\n \"description\": \"`[shell]` — the body of a `kind = \\\"shell\\\"` adapter (a driven surface).\",\n \"type\": \"object\",\n \"properties\": {\n \"spawn\": {\n \"description\": \"Broker-launched opaque spawn command.\",\n \"type\": \"string\"\n },\n \"ephemeral\": {\n \"description\": \"Ephemeral ⇒ no offline perch + no history retention.\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"broadcast\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Broadcast\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"command_receipt\": {\n \"description\": \"How the shell receives agent commands.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/CommandReceipt\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"pre_close\": {\n \"description\": \"Instruction sent to the binary on link-break.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"close_timeout_ms\": {\n \"description\": \"Graceful-termination window before force-close.\",\n \"type\": [\n \"integer\",\n \"null\"\n ],\n \"format\": \"uint64\",\n \"minimum\": 0\n },\n \"persistent\": {\n \"description\": \"Auto-online whenever the owner endpoint is online.\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"wake_command\": {\n \"description\": \"Long-running wake-watcher run WHILE offline; exit ⇒ revive.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"can_shutdown\": {\n \"description\": \"Whether the shell may fire `api owner-shutdown` to suspend its owner.\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"require_approval\": {\n \"description\": \"Per-spawn user approval gate (floor; a node/endpoint setting may tighten).\\nAbsent ⇒ `none`. (Modeled now; conducted when shells land.)\",\n \"$ref\": \"#/$defs/ShellApproval\"\n },\n \"max_instances_per_owner\": {\n \"description\": \"Optional ceiling on concurrent-existing instances per owner endpoint\\n(online + offline both count). Absent ⇒ unlimited.\",\n \"type\": [\n \"integer\",\n \"null\"\n ],\n \"format\": \"uint32\",\n \"minimum\": 0\n },\n \"over_cap\": {\n \"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.\",\n \"$ref\": \"#/$defs/OverCap\"\n },\n \"capabilities\": {\n \"description\": \"`[shell.capabilities]` — the command vocabulary (agent→shell).\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/$defs/ShellCapability\"\n }\n },\n \"sensory\": {\n \"description\": \"`[shell.sensory]` — the sensory vocabulary (shell→agent).\",\n \"$ref\": \"#/$defs/Sensory\"\n },\n \"drive\": {\n \"description\": \"`[shell.drive]` — the drive vocabulary (agent→shell, REST-only ephemeral\\ncontrol). The owner→shell mirror of `[shell.sensory]`.\",\n \"$ref\": \"#/$defs/Drive\"\n },\n \"tunnel\": {\n \"description\": \"`[shell.tunnel]` — opt-in for the opaque reliable-ordered byte tunnel.\\nAbsent ⇒ no tunnel.\",\n \"$ref\": \"#/$defs/Tunnel\"\n }\n },\n \"required\": [\n \"spawn\"\n ]\n },\n \"Broadcast\": {\n \"type\": \"string\",\n \"enum\": [\n \"subnet\",\n \"same-node\",\n \"none\"\n ]\n },\n \"CommandReceipt\": {\n \"type\": \"string\",\n \"enum\": [\n \"http\",\n \"stdin\",\n \"relay\"\n ]\n },\n \"ShellApproval\": {\n \"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).\",\n \"oneOf\": [\n {\n \"description\": \"No approval (default — matches the system's everything-opt-in posture).\",\n \"type\": \"string\",\n \"const\": \"none\"\n },\n {\n \"description\": \"Prompt; allow-always persists a grant, later spawns auto-allow.\",\n \"type\": \"string\",\n \"const\": \"remembered\"\n },\n {\n \"description\": \"Prompt on every spawn; allow-always suppressed (no persistent grant).\",\n \"type\": \"string\",\n \"const\": \"always\"\n }\n ]\n },\n \"OverCap\": {\n \"description\": \"What happens when an owner is at its `max_instances_per_owner` cap.\",\n \"oneOf\": [\n {\n \"description\": \"Refuse the spawn outright (default).\",\n \"type\": \"string\",\n \"const\": \"reject\"\n },\n {\n \"description\": \"Require per-spawn approval beyond the cap (does not raise the cap).\",\n \"type\": \"string\",\n \"const\": \"approve\"\n }\n ]\n },\n \"ShellCapability\": {\n \"description\": \"One entry in `[shell.capabilities]` — a command and its argument names.\\n\\nA capability may carry its own **act-gate**: the same `require_approval`\\nenum 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\\\").\",\n \"type\": \"object\",\n \"properties\": {\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"require_approval\": {\n \"description\": \"Per-act approval gate (floor; a node/endpoint setting may tighten).\\nAbsent ⇒ `none` (ungated).\",\n \"$ref\": \"#/$defs/ShellApproval\"\n },\n \"class_key\": {\n \"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`.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n }\n }\n },\n \"Sensory\": {\n \"description\": \"`[shell.sensory]` — the sensory payload types a shell may emit.\",\n \"type\": \"object\",\n \"properties\": {\n \"types\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"Drive\": {\n \"description\": \"`[shell.drive]` — the drive payload types an agent may push to a shell. The\\nowner→shell mirror of [`Sensory`]: REST-only, ephemeral latest-wins, never\\nspooled.\",\n \"type\": \"object\",\n \"properties\": {\n \"types\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"Tunnel\": {\n \"description\": \"`[shell.tunnel]` — the opt-in for the opaque reliable-ordered byte tunnel: a\\ndedicated QUIC stream pair bound to the owner↔shell link, carrying wire\\nprotocol 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.\",\n \"type\": \"object\",\n \"properties\": {\n \"enable\": {\n \"description\": \"Whether this shell opens the opaque tunnel on link-up.\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"protocol\": {\n \"description\": \"Optional diagnostic label for the opaque wire protocol (e.g. `usbip-urb`).\\nInformational only — the substrate never interprets tunnel bytes.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n }\n }\n },\n \"Hint\": {\n \"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.\",\n \"type\": \"object\",\n \"properties\": {\n \"keywords\": {\n \"description\": \"Keywords that fire the hint — literal **case-insensitive substrings** by\\ndefault; compiled as **regex** patterns when `regex = true`.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"text\": {\n \"description\": \"The hint text surfaced when a keyword matches.\",\n \"type\": \"string\"\n },\n \"regex\": {\n \"description\": \"Treat `keywords` as regex patterns instead of literal substrings.\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\n \"text\"\n ]\n }\n },\n \"$id\": \"http://localhost:5474/manifest.schema.json\"\n}\n" right: "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"spt-core runtime manifest\",\n \"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.\",\n \"type\": \"object\",\n \"properties\": {\n \"adapter\": {\n \"$ref\": \"#/$defs/Adapter\"\n },\n \"hooks\": {\n \"description\": \"`[hooks.]` — inbound hook table, keyed by harness event name.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/$defs/Hook\"\n }\n },\n \"session\": {\n \"description\": \"`[session]` — watched-dir keys plus the `[session.]` templates.\",\n \"$ref\": \"#/$defs/Session\"\n },\n \"env\": {\n \"description\": \"`[env.]` — env-var inject/read table.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"$ref\": \"#/$defs/EnvVar\"\n }\n },\n \"history\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/History\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"digest\": {\n \"description\": \"`[digest]` — the adapter-declared session-digest extractor seam.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Digest\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"inject\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Inject\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"io\": {\n \"description\": \"`[io]` — the adapter's IO-funnel compliance declaration. Absent ⇒ core\\nparses NOTHING of this adapter's ingest, which makes migration safe.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Io\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"message-idle-translation-binary\": {\n \"description\": \"`[message-idle-translation-binary]` — opt-in adapter idle-delivery\\ntranslation binary. A TABLE carrying a `path` scalar (modeled as a table,\\nnot 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 delivery path has been removed).\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/IdleTranslationBinary\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"service\": {\n \"description\": \"`[service]` — the daemon-supervised ResidentService this adapter owns.\\nAbsent ⇒ the adapter has no supervised binary. Independent of\\n`adapter.kind`: a shell adapter's service is the shape the first consumer\\nships, but nothing about the substrate requires a shell.\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Service\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"identity\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Identity\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"update\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Update\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"shell\": {\n \"description\": \"`[shell]` body — present iff `adapter.kind = \\\"shell\\\"` (validated).\",\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/Shell\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"profiles\": {\n \"description\": \"`[profiles.]` — **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`:` selects one. **Local** (node-local, user-authored)\\nprofiles live beside the adapter in the registry, never here.\\n(CONTEXT.md §adapter profile.)\",\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"strings\": {\n \"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.)\",\n \"type\": \"object\",\n \"additionalProperties\": true\n },\n \"hints\": {\n \"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).\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/Hint\"\n }\n }\n },\n \"required\": [\n \"adapter\"\n ],\n \"$defs\": {\n \"Adapter\": {\n \"description\": \"`[adapter]` — the manifest header, readable before any update (compat gate).\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"kind\": {\n \"$ref\": \"#/$defs/AdapterKind\",\n \"default\": \"harness\"\n },\n \"version\": {\n \"type\": \"string\"\n },\n \"min_spt_core_version\": {\n \"description\": \"Lowest spt-core version this adapter tolerates (compat gate).\",\n \"type\": \"string\"\n },\n \"hostable_types\": {\n \"description\": \"Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"shortcut_basename\": {\n \"description\": \"Optional basename for the `spt endpoint run` picker's `-`\\nlauncher shortcut. Absent ⇒ the harness-agnostic default\\n`spt` (→ `spt-`); an adapter sets this to brand its shortcuts\\n(spt-claude-code → `cc`, giving `cc-`). Additive + N-1-safe (omitted\\nfrom serialization when absent). The picker reads it from the RESOLVED\\nmanifest of the selected adapter.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"web_short_path\": {\n \"description\": \"Optional served-name alias for the base adapter's core-owned `web/` root.\\nThe adapter facet exists even when this field is absent.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"docs_dir\": {\n \"description\": \"Optional ADAPTER-RELATIVE directory holding the adapter's own docs,\\nserved under the reserved `docs` segment of its adapter facet\\n(`//a//docs/…`). Absent ⇒ that segment answers 404 naming\\nthe facet; it never falls through to the core-owned `web/` root, which\\nkeeps the meaning of an already-shipped URL fixed. Absolute paths and\\nparent-directory escapes are refused by name at load AND re-resolved at\\nrequest time.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"host_binaries\": {\n \"description\": \"The harness executable basenames this `kind=\\\"harness\\\"` adapter hosts\\nagents inside (e.g. `host_binaries = [\\\"claude\\\"]`). This is the bind-time\\nadapter-resolution match-key: a harness session's parent pid → its exe\\nbasename 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`).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"summary\": {\n \"description\": \"Optional one-paragraph plain-text summary of what the adapter is, at\\nmost 300 characters and free of control characters (newlines\\nincluded). Additive: a manifest without it stays valid.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"changelog\": {\n \"description\": \"Optional ARCHIVE-RELATIVE path to the adapter's markdown changelog, so\\nthe changelog ships inside the artifact it describes. Install and\\nupdate render it to HTML once and the node serves the result at\\n`//a//changelog`. Absolute paths, parent-directory\\nescapes and non-`.md` names are refused at load; a declared file that\\nis missing or unreadable fails the install.\",\n \"type\": [\n \"string\",\n \"null\"\n ]\n }\n },\n \"required\": [\n \"name\",\n \"version\",\n \"min_spt_core_version\"\n ]\n },\n \"AdapterKind\": {\n \"description\": \"The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven\\nsurface (MANIFEST §Shell adapters).\",\n \"type\": \"string\",\n \"enum\": [\n \"harness\",\n \"shell\"\n ]\n },\n \"Hook\": {\n \"description\": \"`[hooks.]` — one harness event → the `api` command it fires, the\\nstdin fields it maps, and whether it can surface context to the agent.\",\n \"type\": \"object\",\n \"properties\": {\n \"fires\": {\n \"description\": \"Opaque `api …` command line the harness invokes for this event.\",\n \"type\": \"string\"\n },\n \"reads\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"can_inject\": {\n \"description\": \"Whether this hook can inject context (false ⇒ sentinel/relay fallback).\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\n \"fires\"\n ]\n },\n \"Session\": {\n \"description\": \"`[session]` — the watched-dir keys (`commune_dir`/`signoff_dir`) co-located\\nwith the fixed set of `[session.]` command templates.\",\n \"type\": \"object\",\n \"properties\": {\n \"commune_dir\": {\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"signoff_dir\": {\n \"type\": [\n \"string\",\n \"null\"\n ]\n },\n \"self\": {\n \"anyOf\": [\n {\n \"$ref\": \"#/$defs/SessionRole\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"resume\": {\n \"description\": \"`[session.resu