Write

{
  "$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"
        }
      ]
    },
    "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 overlays declared by the adapter dev inside the parent manifest, updating as one unit with it. Stored raw ([`toml::Value`]); [`crate::profile::resolve`] merges one onto the base and re-validates the complete manifest. A bare `adapter_name` ignores these (parent unmodified); the composite `<adapter>:<profile>` selects one. **Local** (node-local, user-authored) profiles live beside the adapter in the registry, never here. (CONTEXT.md §adapter profile.)",
      "type": "object",
      "additionalProperties": true
    },
    "strings": {
      "description": "`[strings]` — an adapter-authored KV tree of **opaque data** (spt-core never executes a string; command templates live in their own sections behind registration). Dot-path-readable via `spt adapter get-string`, and it rides the same leaf-replace profile overlay as the rest of the manifest (a shipped or local profile may override base strings). Node-local; no cross-node sync. (CONTEXT.md §adapter strings.)",
      "type": "object",
      "additionalProperties": true
    },
    "hints": {
      "description": "`[[hints]]` — once-per-session keyword hints (CONTEXT.md §keyword hints). **Order is significant** (first match wins). A profile overlays this by leaf-replace like any section — the array is replaced wholesale, never spliced (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>` launcher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default `spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts (spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted from serialization when absent). The picker reads it from the RESOLVED manifest of the selected adapter.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "name",
        "version",
        "min_spt_core_version"
      ]
    },
    "AdapterKind": {
      "description": "The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven surface (MANIFEST §Shell adapters).",
      "type": "string",
      "enum": [
        "harness",
        "shell"
      ]
    },
    "Hook": {
      "description": "`[hooks.<event>]` — one harness event → the `api` command it fires, the stdin 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 with 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"
            }
          ]
        },
        "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 (ADR-0007's `notif_command` seam, REQ-NOTIF-2): an OS toast, a GameRobot `alert-symbol`, anything the adapter can run. Spawned detached when a notif surfaces at this endpoint, combinable with the agent-surface delivery. Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.",
          "anyOf": [
            {
              "$ref": "#/$defs/SessionRole"
            },
            {
              "type": "null"
            }
          ]
        }
      }
    },
    "SessionRole": {
      "description": "`[session.<role>]` — one opaque outbound command template plus its spawn context. Model/tools/flags all live inside `command`, never as fields (MANIFEST §session roles). No nested tables here (keeps TOML round-trip emission 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 guard).",
          "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 \"no manifest seam\" stance: the digest gets its **own** adapter-declared extractor, distinct from `[history]` (which stays opaque + single-session and feeds the echo-commune verbatim). The extractor maps the harness's **native** log → the published `{role, text, tool, ts}` digest-record contract ([`spt_term::DigestRecord`]). **Imperative, not a DSL** (ADR-0019 §Decision): real harness logs are nested (one line → many entries, mixed block lists, types to filter) — a flat declarative map cannot express them, and a map powerful enough is a reinvented language. So the extractor is an opaque command spt-core never parses, exactly like every other manifest template. **Source.** By default the extractor reads the **same files as `[history]`** (the `locate_template`; DRY). An adapter may override with `source` (the own-source escape hatch). `api digest-entry` push remains the always-available fallback for a log-less adapter (which declares no `[digest]` at all). **Presentation.** `window_turns`, `arg_truncation`, and `sprint_collapse` are adapter-declared **defaults** any consumer may override at pull/subscribe; spt-core ships fallback defaults ([`spt_term::DigestConfig`]) when absent. The fixed \"~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}` contract (one JSON record per output line). `{key}` substitution applies (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 extractor 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. Absent ⇒ 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"
      ]
    },
    "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` avenue: source repo.",
          "type": [
            "string",
            "null"
          ]
        },
        "path_regex": {
          "description": "`file_pull` avenue: path selector.",
          "type": [
            "string",
            "null"
          ]
        },
        "signing_key": {
          "description": "`file_pull` avenue: the adapter's Ed25519 **content-signing public key** (64 hex chars / 32 bytes). spt-core verifies a pulled payload against this per-adapter key before applying it (REQ-UPD-5 adapter content signing, ADR-0004 §D) — the adapter author signs their own releases; spt-core's release key stays scoped to spt-core. **Required for `file_pull`** (there are bytes to verify); not applicable to `delegated` (opaque updater).",
          "type": [
            "string",
            "null"
          ]
        },
        "self_verifies": {
          "description": "`delegated` avenue: the adapter attests its own updater verifies the content it installs (e.g. `claude.exe plugin update` checks its own signatures). spt-core cannot see a delegated updater's bytes, so it delegates the trust **only** when this is set; an unattested delegated update 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 is quiesced (the mirror of `spt adapter add`, which reuses this section as the install mechanism). Absent ⇒ spt-core's default cleanup. (Modeled in M2a; conducted with adapter-registration later.)",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "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"
        }
      ]
    },
    "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). Absent ⇒ `none`. (Modeled now; conducted when shells land.)",
          "$ref": "#/$defs/ShellApproval"
        },
        "max_instances_per_owner": {
          "description": "Optional ceiling on concurrent-existing instances per owner endpoint (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 approval 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"
        }
      },
      "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 plumbing: `remembered` lets allow-always write a persistent grant; `always` suppresses 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.",
      "type": "object",
      "properties": {
        "args": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "Sensory": {
      "description": "`[shell.sensory]` — the sensory payload types a shell may emit.",
      "type": "object",
      "properties": {
        "types": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "Hint": {
      "description": "`[[hints]]` — one once-per-session keyword hint (CONTEXT.md §keyword hints). The adapter's user-prompt hook pipes the full user message to `spt api hint`; a matching keyword surfaces `text` to the agent's context channel, at most once per session and once per message.",
      "type": "object",
      "properties": {
        "keywords": {
          "description": "Keywords that fire the hint — literal **case-insensitive substrings** by default; 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"
}


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\adapter\manifest.schema.json (file state is current in your context — no need to Read it back)

spt-core runtime manifest

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.

`[hooks.<event>]` — inbound hook table, keyed by harness event name.

`[session]` — watched-dir keys plus the `[session.<role>]` templates.

`[env.<VAR>]` — env-var inject/read table.

`[digest]` — the adapter-declared session-digest extractor seam (ADR-0019).

`[shell]` body — present iff `adapter.kind = "shell"` (validated).

`[profiles.<name>]` — **shipped** profile overlays: sparse leaf-replace overlays declared by the adapter dev inside the parent manifest, updating as one unit with it. Stored raw ([`toml::Value`]); [`crate::profile::resolve`] merges one onto the base and re-validates the complete manifest. A bare `adapter_name` ignores these (parent unmodified); the composite `<adapter>:<profile>` selects one. **Local** (node-local, user-authored) profiles live beside the adapter in the registry, never here. (CONTEXT.md §adapter profile.)

`[strings]` — an adapter-authored KV tree of **opaque data** (spt-core never executes a string; command templates live in their own sections behind registration). Dot-path-readable via `spt adapter get-string`, and it rides the same leaf-replace profile overlay as the rest of the manifest (a shipped or local profile may override base strings). Node-local; no cross-node sync. (CONTEXT.md §adapter strings.)

`[[hints]]` — once-per-session keyword hints (CONTEXT.md §keyword hints). **Order is significant** (first match wins). A profile overlays this by leaf-replace like any section — the array is replaced wholesale, never spliced (override/extend = re-declare).

`[adapter]` — the manifest header, readable before any update (compat gate).

Lowest spt-core version this adapter tolerates (compat gate).

Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).

Optional basename for the `spt endpoint run` picker's `<basename>-<id>` launcher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default `spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts (spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted from serialization when absent). The picker reads it from the RESOLVED manifest of the selected adapter.

The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven surface (MANIFEST §Shell adapters).

`[hooks.<event>]` — one harness event → the `api` command it fires, the stdin fields it maps, and whether it can surface context to the agent.

Opaque `api …` command line the harness invokes for this event.

Whether this hook can inject context (false ⇒ sentinel/relay fallback).

`[session]` — the watched-dir keys (`commune_dir`/`signoff_dir`) co-located with the fixed set of `[session.<role>]` command templates.

`[session.notif]` — the endpoint-native notification render (ADR-0007's `notif_command` seam, REQ-NOTIF-2): an OS toast, a GameRobot `alert-symbol`, anything the adapter can run. Spawned detached when a notif surfaces at this endpoint, combinable with the agent-surface delivery. Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.

`[session.<role>]` — one opaque outbound command template plus its spawn context. Model/tools/flags all live inside `command`, never as fields (MANIFEST §session roles). No nested tables here (keeps TOML round-trip emission scalar-before-table clean).

Opaque command line, with `{key}` substitution placeholders.

Env var set on summarizer children so their own hooks bail (recursion guard).

Env vars to strip from the child's inherited environment.

Substitution keys spt-core guarantees to fill for this role.

`[env.<VAR>]` — a single env-var directive.

Value to inject (with substitution); required for `inject`.

Harness-hosted injection channel (spt-hosted inherits from the broker).

`[history]` — transcript access strategy.

`fetcher` strategy: adapter binary emitting normalized history.

`locate_normalize` strategy: where the raw transcript lives.

`locate_normalize` strategy: command normalizing the raw transcript.

spt-core asks the adapter (pull, adapter binary emits normalized).

spt-core locates the raw transcript then normalizes it.

Adapter pushes via `api history-log`; spt-core stores (Path-B).

`[digest]` — the session-digest extractor seam (ADR-0019). Reverses M9's "no manifest seam" stance: the digest gets its **own** adapter-declared extractor, distinct from `[history]` (which stays opaque + single-session and feeds the echo-commune verbatim). The extractor maps the harness's **native** log → the published `{role, text, tool, ts}` digest-record contract ([`spt_term::DigestRecord`]). **Imperative, not a DSL** (ADR-0019 §Decision): real harness logs are nested (one line → many entries, mixed block lists, types to filter) — a flat declarative map cannot express them, and a map powerful enough is a reinvented language. So the extractor is an opaque command spt-core never parses, exactly like every other manifest template. **Source.** By default the extractor reads the **same files as `[history]`** (the `locate_template`; DRY). An adapter may override with `source` (the own-source escape hatch). `api digest-entry` push remains the always-available fallback for a log-less adapter (which declares no `[digest]` at all). **Presentation.** `window_turns`, `arg_truncation`, and `sprint_collapse` are adapter-declared **defaults** any consumer may override at pull/subscribe; spt-core ships fallback defaults ([`spt_term::DigestConfig`]) when absent. The fixed "~3 turns" is no longer an spt-core requirement (ADR-0019).

Opaque extractor command: native harness log → the `{role,text,tool,ts}` contract (one JSON record per output line). `{key}` substitution applies (e.g. `{session_id}`, and `{source}` for the resolved log path).

Own-source escape hatch: a `locate_template` for the log file(s) the extractor reads. Absent ⇒ reuse `[history].locate_template` (DRY).

Adapter-default window depth (user turns kept). Absent ⇒ spt-core fallback.

Adapter-default tool-arg truncation width. Absent ⇒ spt-core fallback.

Adapter-default for collapsing consecutive tool records into one sprint. Absent ⇒ spt-core fallback (collapse on).

`[inject]` — inject-input methods per activity state.

`[identity]` — how the harness's session id is obtained.

Process-tree anchor name when `session_id` is absent.

Discovered after spawn (process-tree / wrapper handoff).

Injected as a UUID the harness echoes back.

`[update]` — adapter self-update directive (parsed in M2a; conducted in M3).

`delegated` avenue: the command spt-core delegates to.

`file_pull` avenue: source repo.

`file_pull` avenue: path selector.

`file_pull` avenue: the adapter's Ed25519 **content-signing public key** (64 hex chars / 32 bytes). spt-core verifies a pulled payload against this per-adapter key before applying it (REQ-UPD-5 adapter content signing, ADR-0004 §D) — the adapter author signs their own releases; spt-core's release key stays scoped to spt-core. **Required for `file_pull`** (there are bytes to verify); not applicable to `delegated` (opaque updater).

`delegated` avenue: the adapter attests its own updater verifies the content it installs (e.g. `claude.exe plugin update` checks its own signatures). spt-core cannot see a delegated updater's bytes, so it delegates the trust **only** when this is set; an unattested delegated update is skipped as unverifiable (REQ-UPD-5).

Verify spt-core satisfies `min_spt_core_version` before/after.

Optional inverse of install — run by `spt adapter remove` once the adapter is quiesced (the mirror of `spt adapter add`, which reuses this section as the install mechanism). Absent ⇒ spt-core's default cleanup. (Modeled in M2a; conducted with adapter-registration later.)

Delegate to the adapter's own updater (e.g. `claude plugin update`).

spt-core pulls files from a repo.

`[shell]` — the body of a `kind = "shell"` adapter (a driven surface).

Broker-launched opaque spawn command.

Ephemeral ⇒ no offline perch + no history retention.

How the shell receives agent commands.

Instruction sent to the binary on link-break.

Graceful-termination window before force-close.

Auto-online whenever the owner endpoint is online.

Long-running wake-watcher run WHILE offline; exit ⇒ revive.

Whether the shell may fire `api owner-shutdown` to suspend its owner.

Per-spawn user approval gate (floor; a node/endpoint setting may tighten). Absent ⇒ `none`. (Modeled now; conducted when shells land.)

Optional ceiling on concurrent-existing instances per owner endpoint (online + offline both count). Absent ⇒ unlimited.

What happens at the cap: `reject` (default) or `approve` (per-spawn approval beyond the cap; does not raise it). Only meaningful with a cap.

`[shell.capabilities]` — the command vocabulary (agent→shell).

`[shell.sensory]` — the sensory vocabulary (shell→agent).

Per-shell instantiation-approval mode (`require_approval`). Reuses the consent plumbing: `remembered` lets allow-always write a persistent grant; `always` suppresses allow-always (prompt every spawn).

No approval (default — matches the system's everything-opt-in posture).

Prompt; allow-always persists a grant, later spawns auto-allow.

Prompt on every spawn; allow-always suppressed (no persistent grant).

What happens when an owner is at its `max_instances_per_owner` cap.

Refuse the spawn outright (default).

Require per-spawn approval beyond the cap (does not raise the cap).

One entry in `[shell.capabilities]` — a command and its argument names.

`[shell.sensory]` — the sensory payload types a shell may emit.

`[[hints]]` — one once-per-session keyword hint (CONTEXT.md §keyword hints). The adapter's user-prompt hook pipes the full user message to `spt api hint`; a matching keyword surfaces `text` to the agent's context channel, at most once per session and once per message.

Keywords that fire the hint — literal **case-insensitive substrings** by default; compiled as **regex** patterns when `regex = true`.

The hint text surfaced when a keyword matches.

Treat `keywords` as regex patterns instead of literal substrings.


Now the manifest with provenance header + impl tag.


# claude-spt — Claude Code adapter manifest (publish target #2: the spt-core adapter registry side).
#
# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile
# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the
# marketplace `plugin.json` version.
#
# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/
# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the
# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).
#
# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not
# yet built, or needs a capability not on the public surface — see notes inline):
#   * [session] + [session.<role>]  — live-agent / Psyche / commune / signoff / notif spawn
#     templates. Land with the M12 parity surfaces (/sptc:live, commune, signoff).
#   * [history]                     — transcript-access seam. Lands with [digest]'s sibling fetcher.
#   * [update]                      — distribution self-update. `file_pull` needs (a) a published
#     adapter-registry repo target and (b) an Ed25519 content-signing key we do not yet hold;
#     `delegated` to `claude plugin update` needs CC to attest signature verification
#     (`self_verifies`) — unconfirmed on the public surface. Tracked as a finding
#     (docs/SPT-CORE-FINDINGS.md) rather than guessed here.
#
# [impl->REQ-DIST-MANIFEST-SCHEMA]

[adapter]
name = "claude-spt"
kind = "harness"
version = "0.1.0"
# Local spt binary is 0.6.0; the published contract we build to is v0.7.0 (manifest.schema.json,
# shortcut_basename). 0.7.0 is the floor this manifest's fields require.
min_spt_core_version = "0.7.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# Brands the `spt endpoint run` picker shortcut as `sptc-<id>` (default would be `spt-<id>`).
# `sptc` is the single-substitution succession seam (ADR-0001): parity-proof flips s/sptc/spt/.
shortcut_basename = "sptc"

# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.

[hooks.SessionStart]
# Seed the endpoint. pid is sourced from the hook PROCESS tree, not stdin (F-001); session_id is
# a CC stdin field. NOT the blocking poll/listen loop — that is /sptc:ready|live, never SessionStart.
fires = "api seed --session-id {session_id} --pid {parent_pid}"
reads = ["session_id"]
can_inject = true

[hooks.UserPromptSubmit]
# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
fires = "api poll {session_id}"
reads = ["session_id", "prompt"]
can_inject = true

[hooks.Stop]
# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
fires = "api state idle"
reads = ["session_id"]
can_inject = false

[hooks.SessionEnd]
fires = "api session-end {session_id}"
reads = ["session_id"]
can_inject = false

[hooks.SubagentStart]
fires = "api worker-start {session_id}"
reads = ["session_id"]
can_inject = false

[hooks.SubagentStop]
fires = "api worker-stop {session_id}"
reads = ["session_id"]
can_inject = false

# ── [identity] ─────────────────────────────────────────────────────────────────────────────
[identity]
# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
session_id_source = "post_spawn"
# Process-tree anchor when session_id is absent: the Claude Code binary.
parent_ancestor_name = "claude"

# ── [inject] ───────────────────────────────────────────────────────────────────────────────
[inject]
# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
activity = ["hook"]
idle = ["hook"]

# ── [digest] ───────────────────────────────────────────────────────────────────────────────
# Session-digest extractor seam (ADR-0019). CC's transcript is JSONL (one event per line); the
# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
# one JSON record per output line. The extractor binary self-locates CC's transcript from the
# session id (no [history] locate_template declared this slice), so no {source} placeholder.
# The extractor binary ships in the spt-core-conducted layer (built in a later parity slice);
# this is its declaration.
[digest]
extractor = "claude-spt-digest --session {session_id}"
window_turns = 6
arg_truncation = 240
sprint_collapse = true

# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
# `deep` widens the digest window for long-context review sessions without forking the base.
# (Node-local user profiles are created with `spt adapter create-profile` and live in the
# registry, never in this manifest.)
[profiles.deep.digest]
window_turns = 20

# ── [strings] ──────────────────────────────────────────────────────────────────────────────
# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
[strings]
adapter_label = "Claude Code (spt)"

[strings.notice]
not_installed = "spt-core not found on PATH — run /sptc:setup to install it."

[strings.skills]
# One-line summaries (the operative multi-line bodies arrive file-backed in the M12 slice).
commune = "Send a communal context update to your Psyche."
live = "Run or resume a live agent session."
ready = "Enable inter-agent messaging for this session."
send = "Send a message to another spt agent."
signoff = "Graceful live-agent shutdown with a final context summary."
whoami = "Report this session's spt endpoint id."
version = "Report the spt-core-tracked adapter/binary version-of-truth."

# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
[[hints]]
keywords = ["go live", "live agent", "start live"]
text = "Tip: /sptc:live starts or resumes a live agent session."

[[hints]]
keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
text = "Tip: /sptc:whoami reports this session's spt endpoint id."

[[hints]]
keywords = ["message another agent", "send to agent", "tell the other agent"]
text = "Tip: /sptc:send messages another spt agent; /sptc:ready first to receive replies."


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\adapter\claude-spt.toml (file state is current in your context — no need to Read it back)

# claude-spt — Claude Code adapter manifest (publish target #2: the spt-core adapter registry side).
#
# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile
# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the
# marketplace `plugin.json` version.
#
# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/
# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the
# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).
#
# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not
# yet built, or needs a capability not on the public surface — see notes inline):
#   * [session] + [session.<role>]  — live-agent / Psyche / commune / signoff / notif spawn
#     templates. Land with the M12 parity surfaces (/sptc:live, commune, signoff).
#   * [history]                     — transcript-access seam. Lands with [digest]'s sibling fetcher.
#   * [update]                      — distribution self-update. `file_pull` needs (a) a published
#     adapter-registry repo target and (b) an Ed25519 content-signing key we do not yet hold;
#     `delegated` to `claude plugin update` needs CC to attest signature verification
#     (`self_verifies`) — unconfirmed on the public surface. Tracked as a finding
#     (docs/SPT-CORE-FINDINGS.md) rather than guessed here.
#
# [impl->REQ-DIST-MANIFEST-SCHEMA]

[adapter]
name = "claude-spt"
kind = "harness"
version = "0.1.0"
# Local spt binary is 0.6.0; the published contract we build to is v0.7.0 (manifest.schema.json,
# shortcut_basename). 0.7.0 is the floor this manifest's fields require.
min_spt_core_version = "0.7.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# Brands the `spt endpoint run` picker shortcut as `sptc-<id>` (default would be `spt-<id>`).
# `sptc` is the single-substitution succession seam (ADR-0001): parity-proof flips s/sptc/spt/.
shortcut_basename = "sptc"

# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.

[hooks.SessionStart]
# Seed the endpoint. pid is sourced from the hook PROCESS tree, not stdin (F-001); session_id is
# a CC stdin field. NOT the blocking poll/listen loop — that is /sptc:ready|live, never SessionStart.
fires = "api seed --session-id {session_id} --pid {parent_pid}"
reads = ["session_id"]
can_inject = true

[hooks.UserPromptSubmit]
# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
fires = "api poll {session_id}"
reads = ["session_id", "prompt"]
can_inject = true

[hooks.Stop]
# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
fires = "api state idle"
reads = ["session_id"]
can_inject = false

[hooks.SessionEnd]
fires = "api session-end {session_id}"
reads = ["session_id"]
can_inject = false

[hooks.SubagentStart]
fires = "api worker-start {session_id}"
reads = ["session_id"]
can_inject = false

[hooks.SubagentStop]
fires = "api worker-stop {session_id}"
reads = ["session_id"]
can_inject = false

# ── [identity] ─────────────────────────────────────────────────────────────────────────────
[identity]
# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
session_id_source = "post_spawn"
# Process-tree anchor when session_id is absent: the Claude Code binary.
parent_ancestor_name = "claude"

# ── [inject] ───────────────────────────────────────────────────────────────────────────────
[inject]
# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
activity = ["hook"]
idle = ["hook"]

# ── [digest] ───────────────────────────────────────────────────────────────────────────────
# Session-digest extractor seam (ADR-0019). CC's transcript is JSONL (one event per line); the
# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
# one JSON record per output line. The extractor binary self-locates CC's transcript from the
# session id (no [history] locate_template declared this slice), so no {source} placeholder.
# The extractor binary ships in the spt-core-conducted layer (built in a later parity slice);
# this is its declaration.
[digest]
extractor = "claude-spt-digest --session {session_id}"
window_turns = 6
arg_truncation = 240
sprint_collapse = true

# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
# `deep` widens the digest window for long-context review sessions without forking the base.
# (Node-local user profiles are created with `spt adapter create-profile` and live in the
# registry, never in this manifest.)
[profiles.deep.digest]
window_turns = 20

# ── [strings] ──────────────────────────────────────────────────────────────────────────────
# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
[strings]
adapter_label = "Claude Code (spt)"

[strings.notice]
not_installed = "spt-core not found on PATH — run /sptc:setup to install it."

[strings.skills]
# One-line summaries (the operative multi-line bodies arrive file-backed in the M12 slice).
commune = "Send a communal context update to your Psyche."
live = "Run or resume a live agent session."
ready = "Enable inter-agent messaging for this session."
send = "Send a message to another spt agent."
signoff = "Graceful live-agent shutdown with a final context summary."
whoami = "Report this session's spt endpoint id."
version = "Report the spt-core-tracked adapter/binary version-of-truth."

# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
[[hints]]
keywords = ["go live", "live agent", "start live"]
text = "Tip: /sptc:live starts or resumes a live agent session."

[[hints]]
keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
text = "Tip: /sptc:whoami reports this session's spt endpoint id."

[[hints]]
keywords = ["message another agent", "send to agent", "tell the other agent"]
text = "Tip: /sptc:send messages another spt agent; /sptc:ready first to receive replies."


Now the validator + sh wrapper (gate). Validator picks toml lib at runtime.


#!/usr/bin/env python3
"""Validate an adapter manifest (TOML) against spt-core's published manifest.schema.json.

Deterministic, offline: the schema is the vendored copy of spt-core's published v0.7.0
manifest.schema.json (adapter/manifest.schema.json) — no network, no spt binary, no registry
side effects. This is the schema-validation layer only; spt-core's `spt adapter add` adds a
second cross-field registration-validation layer beyond JSON Schema (see the schema description).

Usage:  validate_manifest.py <schema.json> <manifest.toml> [<manifest.toml> ...]
Exit:   0 = all valid · 1 = a manifest failed validation · 2 = usage / missing dependency.

# [impl->REQ-DIST-MANIFEST-SCHEMA]
"""
import json
import sys

# TOML reader: stdlib tomllib (py3.11+) or the tomli backport (py<=3.10). Either covers the fleet.
try:
    import tomllib as _toml  # type: ignore
except ModuleNotFoundError:  # pragma: no cover - depends on interpreter
    try:
        import tomli as _toml  # type: ignore
    except ModuleNotFoundError:
        sys.stderr.write("MISSING-DEP: no TOML reader (need py3.11 tomllib or `pip install tomli`)\n")
        sys.exit(2)

try:
    import jsonschema
except ModuleNotFoundError:  # pragma: no cover
    sys.stderr.write("MISSING-DEP: jsonschema not installed (`pip install jsonschema`)\n")
    sys.exit(2)

def main(argv):
    if len(argv) < 3:
        sys.stderr.write(__doc__.split("\n\n")[2] + "\n")  # the Usage/Exit block
        return 2

    schema_path, manifest_paths = argv[1], argv[2:]
    with open(schema_path, "rb") as fh:
        schema = json.load(fh)

    # Fail fast on a malformed schema rather than silently passing everything.
    validator_cls = jsonschema.validators.validator_for(schema)
    validator_cls.check_schema(schema)
    validator = validator_cls(schema)

    rc = 0
    for manifest_path in manifest_paths:
        with open(manifest_path, "rb") as fh:
            data = _toml.load(fh)
        errors = sorted(validator.iter_errors(data), key=lambda e: list(e.absolute_path))
        if errors:
            rc = 1
            print(f"FAIL: {manifest_path}")
            for err in errors:
                loc = "/".join(str(p) for p in err.absolute_path) or "(root)"
                print(f"  - {loc}: {err.message}")
        else:
            print(f"ok   {manifest_path}")
    return rc

if __name__ == "__main__":
    sys.exit(main(sys.argv))


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\ci\manifest\validate_manifest.py (file state is current in your context — no need to Read it back)

#!/usr/bin/env python3
"""Validate an adapter manifest (TOML) against spt-core's published manifest.schema.json.

Deterministic, offline: the schema is the vendored copy of spt-core's published v0.7.0
manifest.schema.json (adapter/manifest.schema.json) — no network, no spt binary, no registry
side effects. This is the schema-validation layer only; spt-core's `spt adapter add` adds a
second cross-field registration-validation layer beyond JSON Schema (see the schema description).

Usage:  validate_manifest.py <schema.json> <manifest.toml> [<manifest.toml> ...]
Exit:   0 = all valid · 1 = a manifest failed validation · 2 = usage / missing dependency.

# [impl->REQ-DIST-MANIFEST-SCHEMA]
"""
import json
import sys

# TOML reader: stdlib tomllib (py3.11+) or the tomli backport (py<=3.10). Either covers the fleet.
try:
    import tomllib as _toml  # type: ignore
except ModuleNotFoundError:  # pragma: no cover - depends on interpreter
    try:
        import tomli as _toml  # type: ignore
    except ModuleNotFoundError:
        sys.stderr.write("MISSING-DEP: no TOML reader (need py3.11 tomllib or `pip install tomli`)\n")
        sys.exit(2)

try:
    import jsonschema
except ModuleNotFoundError:  # pragma: no cover
    sys.stderr.write("MISSING-DEP: jsonschema not installed (`pip install jsonschema`)\n")
    sys.exit(2)

def main(argv):
    if len(argv) < 3:
        sys.stderr.write(__doc__.split("\n\n")[2] + "\n")  # the Usage/Exit block
        return 2

    schema_path, manifest_paths = argv[1], argv[2:]
    with open(schema_path, "rb") as fh:
        schema = json.load(fh)

    # Fail fast on a malformed schema rather than silently passing everything.
    validator_cls = jsonschema.validators.validator_for(schema)
    validator_cls.check_schema(schema)
    validator = validator_cls(schema)

    rc = 0
    for manifest_path in manifest_paths:
        with open(manifest_path, "rb") as fh:
            data = _toml.load(fh)
        errors = sorted(validator.iter_errors(data), key=lambda e: list(e.absolute_path))
        if errors:
            rc = 1
            print(f"FAIL: {manifest_path}")
            for err in errors:
                loc = "/".join(str(p) for p in err.absolute_path) or "(root)"
                print(f"  - {loc}: {err.message}")
        else:
            print(f"ok   {manifest_path}")
    return rc

if __name__ == "__main__":
    sys.exit(main(sys.argv))


sh wrapper — self-announcing, picks a capable interpreter, SKIP (exit 0) if deps absent, exit 1 only on real validation failure.


#!/bin/sh
# manifest-schema gate: validate the claude-spt adapter manifest against spt-core's published
# (vendored) manifest.schema.json. Deterministic + offline — no network, no spt binary, no
# registry side effects. Self-announcing: prints its own ok/SKIP/FAIL line.
#   exit 0 = valid OR skipped (no capable interpreter) · exit 1 = manifest failed validation.
# [impl->REQ-DIST-MANIFEST-SCHEMA]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
SCHEMA="$ROOT/adapter/manifest.schema.json"
MANIFEST="$ROOT/adapter/claude-spt.toml"
VALIDATOR="$ROOT/ci/manifest/validate_manifest.py"

for f in "$SCHEMA" "$MANIFEST" "$VALIDATOR"; do
  if [ ! -f "$f" ]; then echo "SKIP: missing $f"; exit 0; fi
done

# Pick an interpreter that has BOTH a TOML reader (tomllib|tomli) AND jsonschema. The fleet's
# python is 3.12 (tomllib) and python3 is 3.10 (tomli) — either satisfies the validator.
PY=""
for cand in python python3 py; do
  command -v "$cand" >/dev/null 2>&1 || continue
  if "$cand" - <<'PROBE' >/dev/null 2>&1
import importlib.util as u, sys
have_toml = u.find_spec("tomllib") or u.find_spec("tomli")
sys.exit(0 if (have_toml and u.find_spec("jsonschema")) else 1)
PROBE
  then PY="$cand"; break; fi
done

if [ -z "$PY" ]; then
  echo "SKIP: no python with (tomllib|tomli)+jsonschema on PATH (pip install tomli jsonschema)"
  exit 0
fi

if "$PY" "$VALIDATOR" "$SCHEMA" "$MANIFEST"; then
  echo "ok   manifest validates against published manifest.schema.json"
  exit 0
else
  echo "FAIL: manifest does not satisfy manifest.schema.json"
  exit 1
fi


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\ci\manifest\check-manifest.sh (file state is current in your context — no need to Read it back)

#!/bin/sh
# manifest-schema gate: validate the claude-spt adapter manifest against spt-core's published
# (vendored) manifest.schema.json. Deterministic + offline — no network, no spt binary, no
# registry side effects. Self-announcing: prints its own ok/SKIP/FAIL line.
#   exit 0 = valid OR skipped (no capable interpreter) · exit 1 = manifest failed validation.
# [impl->REQ-DIST-MANIFEST-SCHEMA]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
SCHEMA="$ROOT/adapter/manifest.schema.json"
MANIFEST="$ROOT/adapter/claude-spt.toml"
VALIDATOR="$ROOT/ci/manifest/validate_manifest.py"

for f in "$SCHEMA" "$MANIFEST" "$VALIDATOR"; do
  if [ ! -f "$f" ]; then echo "SKIP: missing $f"; exit 0; fi
done

# Pick an interpreter that has BOTH a TOML reader (tomllib|tomli) AND jsonschema. The fleet's
# python is 3.12 (tomllib) and python3 is 3.10 (tomli) — either satisfies the validator.
PY=""
for cand in python python3 py; do
  command -v "$cand" >/dev/null 2>&1 || continue
  if "$cand" - <<'PROBE' >/dev/null 2>&1
import importlib.util as u, sys
have_toml = u.find_spec("tomllib") or u.find_spec("tomli")
sys.exit(0 if (have_toml and u.find_spec("jsonschema")) else 1)
PROBE
  then PY="$cand"; break; fi
done

if [ -z "$PY" ]; then
  echo "SKIP: no python with (tomllib|tomli)+jsonschema on PATH (pip install tomli jsonschema)"
  exit 0
fi

if "$PY" "$VALIDATOR" "$SCHEMA" "$MANIFEST"; then
  echo "ok   manifest validates against published manifest.schema.json"
  exit 0
else
  echo "FAIL: manifest does not satisfy manifest.schema.json"
  exit 1
fi


Now the unit test. Check an existing test's style to match.


1	#!/bin/sh
2	# Unit tests for the cplugs skeleton validator: it must PASS the real skeleton and FAIL on tampered
3	# copies (leaked runtime state / planted binary / missing SKILL.md) — proving the gate catches
4	# real breakage, not just green-on-green. Run: sh tests/skeleton-validate.sh  (exit 0 = pass).
5	# [unit->REQ-DIST-PLUGIN-SKELETON]
6	ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
7	VALIDATE="$ROOT/ci/publish/validate-skeleton.sh"
8	fail=0
9
10	# 1. Real skeleton is installable.
11	if sh "$VALIDATE" "$ROOT/plugin/sptc" >/dev/null 2>&1; then echo "ok   real skeleton validates"; else echo "FAIL real skeleton rejected"; sh "$VALIDATE" "$ROOT/plugin/sptc"; fail=1; fi
12
13	# Build a clean fixture copy we can tamper.
14	work=$(mktemp -d) || { echo "FAIL mktemp"; exit 1; }
15	trap 'rm -rf "$work"' EXIT INT TERM
16	sk="$work/sptc"
17	mkdir -p "$sk"
18	cp -r "$ROOT/plugin/sptc/.claude-plugin" "$sk/"
19	cp -r "$ROOT/plugin/sptc/hooks" "$sk/"
20	cp -r "$ROOT/plugin/sptc/skills" "$sk/"
21	cp "$ROOT/plugin/sptc/bootstrap.sh" "$sk/" 2>/dev/null || true
22
23	# Sanity: the clean copy validates (else later negatives are meaningless).
24	if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "ok   clean fixture validates"; else echo "FAIL clean fixture rejected"; fail=1; fi
25
26	# 2. Runtime state leaked INTO a published path (hooks/) must FAIL — this is the real publish leak
27	#    (state inside the subset that package-skeleton.sh would copy to the marketplace). State in a
28	#    non-published dir like `.claude/` is intentionally NOT a finding: the packager never copies it.
29	printf '{"last_used":"perri"}\n' > "$sk/hooks/LIVE_AGENT_IDS.json"
30	if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "FAIL leaked LIVE_AGENT_IDS.json not caught"; fail=1; else echo "ok   catches runtime state in published path"; fi
31	rm -f "$sk/hooks/LIVE_AGENT_IDS.json"
32
33	# 3. Planted binary must FAIL.
34	printf 'MZ\0' > "$sk/skills/rogue.exe"
35	if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "FAIL planted .exe not caught"; fail=1; else echo "ok   catches planted binary"; fi
36	rm -f "$sk/skills/rogue.exe"
37
38	# 4. Missing SKILL.md must FAIL.
39	victim=$(find "$sk/skills" -name SKILL.md | head -n1)
40	mv "$victim" "$victim.bak"
41	if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "FAIL missing SKILL.md not caught"; fail=1; else echo "ok   catches missing SKILL.md"; fi
42	mv "$victim.bak" "$victim"
43
44	# 5. Wrong plugin name must FAIL.
45	sed 's/"name": "sptc"/"name": "wrong"/' "$sk/.claude-plugin/plugin.json" > "$sk/.claude-plugin/plugin.json.tmp" && mv "$sk/.claude-plugin/plugin.json.tmp" "$sk/.claude-plugin/plugin.json"
46	if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "FAIL wrong plugin name not caught"; fail=1; else echo "ok   catches wrong plugin name"; fi
47
48	[ "$fail" -eq 0 ] && { echo "SKELETON-VALIDATE OK"; exit 0; } || { echo "SKELETON-VALIDATE FAIL"; exit 1; }
49

#!/bin/sh
# Unit tests for the cplugs skeleton validator: it must PASS the real skeleton and FAIL on tampered
# copies (leaked runtime state / planted binary / missing SKILL.md) — proving the gate catches
# real breakage, not just green-on-green. Run: sh tests/skeleton-validate.sh  (exit 0 = pass).
# [unit->REQ-DIST-PLUGIN-SKELETON]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
VALIDATE="$ROOT/ci/publish/validate-skeleton.sh"
fail=0

# 1. Real skeleton is installable.
if sh "$VALIDATE" "$ROOT/plugin/sptc" >/dev/null 2>&1; then echo "ok   real skeleton validates"; else echo "FAIL real skeleton rejected"; sh "$VALIDATE" "$ROOT/plugin/sptc"; fail=1; fi

# Build a clean fixture copy we can tamper.
work=$(mktemp -d) || { echo "FAIL mktemp"; exit 1; }
trap 'rm -rf "$work"' EXIT INT TERM
sk="$work/sptc"
mkdir -p "$sk"
cp -r "$ROOT/plugin/sptc/.claude-plugin" "$sk/"
cp -r "$ROOT/plugin/sptc/hooks" "$sk/"
cp -r "$ROOT/plugin/sptc/skills" "$sk/"
cp "$ROOT/plugin/sptc/bootstrap.sh" "$sk/" 2>/dev/null || true

# Sanity: the clean copy validates (else later negatives are meaningless).
if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "ok   clean fixture validates"; else echo "FAIL clean fixture rejected"; fail=1; fi

# 2. Runtime state leaked INTO a published path (hooks/) must FAIL — this is the real publish leak
#    (state inside the subset that package-skeleton.sh would copy to the marketplace). State in a
#    non-published dir like `.claude/` is intentionally NOT a finding: the packager never copies it.
printf '{"last_used":"perri"}\n' > "$sk/hooks/LIVE_AGENT_IDS.json"
if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "FAIL leaked LIVE_AGENT_IDS.json not caught"; fail=1; else echo "ok   catches runtime state in published path"; fi
rm -f "$sk/hooks/LIVE_AGENT_IDS.json"

# 3. Planted binary must FAIL.
printf 'MZ\0' > "$sk/skills/rogue.exe"
if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "FAIL planted .exe not caught"; fail=1; else echo "ok   catches planted binary"; fi
rm -f "$sk/skills/rogue.exe"

# 4. Missing SKILL.md must FAIL.
victim=$(find "$sk/skills" -name SKILL.md | head -n1)
mv "$victim" "$victim.bak"
if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "FAIL missing SKILL.md not caught"; fail=1; else echo "ok   catches missing SKILL.md"; fi
mv "$victim.bak" "$victim"

# 5. Wrong plugin name must FAIL.
sed 's/"name": "sptc"/"name": "wrong"/' "$sk/.claude-plugin/plugin.json" > "$sk/.claude-plugin/plugin.json.tmp" && mv "$sk/.claude-plugin/plugin.json.tmp" "$sk/.claude-plugin/plugin.json"
if sh "$VALIDATE" "$sk" >/dev/null 2>&1; then echo "FAIL wrong plugin name not caught"; fail=1; else echo "ok   catches wrong plugin name"; fi

[ "$fail" -eq 0 ] && { echo "SKELETON-VALIDATE OK"; exit 0; } || { echo "SKELETON-VALIDATE FAIL"; exit 1; }


Write the unit test — real manifest passes, tampered fixtures fail.


#!/bin/sh
# Unit tests for the manifest-schema validator: it must PASS the real claude-spt manifest and FAIL
# on tampered copies (missing required field / bad enum / wrong type) — proving the gate catches
# real breakage, not just green-on-green. Run: sh tests/manifest-schema.sh  (exit 0 = pass).
# [unit->REQ-DIST-MANIFEST-SCHEMA]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
SCHEMA="$ROOT/adapter/manifest.schema.json"
MANIFEST="$ROOT/adapter/claude-spt.toml"
VALIDATOR="$ROOT/ci/manifest/validate_manifest.py"

# Same interpreter probe as the gate wrapper — needs (tomllib|tomli)+jsonschema.
PY=""
for cand in python python3 py; do
  command -v "$cand" >/dev/null 2>&1 || continue
  if "$cand" - <<'PROBE' >/dev/null 2>&1
import importlib.util as u, sys
sys.exit(0 if ((u.find_spec("tomllib") or u.find_spec("tomli")) and u.find_spec("jsonschema")) else 1)
PROBE
  then PY="$cand"; break; fi
done
if [ -z "$PY" ]; then echo "SKIP: no python with (tomllib|tomli)+jsonschema"; echo "MANIFEST-SCHEMA OK (skipped)"; exit 0; fi

fail=0
ok()   { echo "ok   $1"; }
bad()  { echo "FAIL $1"; fail=1; }

# 1. The real manifest validates (exit 0).
if "$PY" "$VALIDATOR" "$SCHEMA" "$MANIFEST" >/dev/null 2>&1; then ok "real manifest validates"; else bad "real manifest rejected"; "$PY" "$VALIDATOR" "$SCHEMA" "$MANIFEST"; fi

work=$(mktemp -d) || { echo "FAIL mktemp"; exit 1; }
trap 'rm -rf "$work"' EXIT INT TERM

# helper: write a manifest body to a temp file, expect validator to REJECT it (exit non-zero).
expect_reject() {
  desc=$1; file="$work/m.toml"
  if "$PY" "$VALIDATOR" "$SCHEMA" "$file" >/dev/null 2>&1; then bad "$desc not caught"; else ok "catches $desc"; fi
}

# 2. Missing the required [adapter] table entirely.
cat > "$work/m.toml" <<'EOF'
[strings]
adapter_label = "x"
EOF
expect_reject "missing [adapter]"

# 3. [adapter] missing a required field (min_spt_core_version).
cat > "$work/m.toml" <<'EOF'
[adapter]
name = "claude-spt"
version = "0.1.0"
EOF
expect_reject "missing required adapter.min_spt_core_version"

# 4. Bad enum value for adapter.kind.
cat > "$work/m.toml" <<'EOF'
[adapter]
name = "claude-spt"
kind = "robot"
version = "0.1.0"
min_spt_core_version = "0.7.0"
EOF
expect_reject "invalid adapter.kind enum"

# 5. Wrong type: a hook with no `fires` (required) / wrong shape.
cat > "$work/m.toml" <<'EOF'
[adapter]
name = "claude-spt"
version = "0.1.0"
min_spt_core_version = "0.7.0"
[hooks.SessionStart]
reads = ["session_id"]
EOF
expect_reject "hook missing required `fires`"

# 6. Wrong type: shortcut_basename as an integer.
cat > "$work/m.toml" <<'EOF'
[adapter]
name = "claude-spt"
version = "0.1.0"
min_spt_core_version = "0.7.0"
shortcut_basename = 42
EOF
expect_reject "non-string shortcut_basename"

[ "$fail" -eq 0 ] && { echo "MANIFEST-SCHEMA OK"; exit 0; } || { echo "MANIFEST-SCHEMA FAIL"; exit 1; }


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\tests\manifest-schema.sh (file state is current in your context — no need to Read it back)

#!/bin/sh
# Unit tests for the manifest-schema validator: it must PASS the real claude-spt manifest and FAIL
# on tampered copies (missing required field / bad enum / wrong type) — proving the gate catches
# real breakage, not just green-on-green. Run: sh tests/manifest-schema.sh  (exit 0 = pass).
# [unit->REQ-DIST-MANIFEST-SCHEMA]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
SCHEMA="$ROOT/adapter/manifest.schema.json"
MANIFEST="$ROOT/adapter/claude-spt.toml"
VALIDATOR="$ROOT/ci/manifest/validate_manifest.py"

# Same interpreter probe as the gate wrapper — needs (tomllib|tomli)+jsonschema.
PY=""
for cand in python python3 py; do
  command -v "$cand" >/dev/null 2>&1 || continue
  if "$cand" - <<'PROBE' >/dev/null 2>&1
import importlib.util as u, sys
sys.exit(0 if ((u.find_spec("tomllib") or u.find_spec("tomli")) and u.find_spec("jsonschema")) else 1)
PROBE
  then PY="$cand"; break; fi
done
if [ -z "$PY" ]; then echo "SKIP: no python with (tomllib|tomli)+jsonschema"; echo "MANIFEST-SCHEMA OK (skipped)"; exit 0; fi

fail=0
ok()   { echo "ok   $1"; }
bad()  { echo "FAIL $1"; fail=1; }

# 1. The real manifest validates (exit 0).
if "$PY" "$VALIDATOR" "$SCHEMA" "$MANIFEST" >/dev/null 2>&1; then ok "real manifest validates"; else bad "real manifest rejected"; "$PY" "$VALIDATOR" "$SCHEMA" "$MANIFEST"; fi

work=$(mktemp -d) || { echo "FAIL mktemp"; exit 1; }
trap 'rm -rf "$work"' EXIT INT TERM

# helper: write a manifest body to a temp file, expect validator to REJECT it (exit non-zero).
expect_reject() {
  desc=$1; file="$work/m.toml"
  if "$PY" "$VALIDATOR" "$SCHEMA" "$file" >/dev/null 2>&1; then bad "$desc not caught"; else ok "catches $desc"; fi
}

# 2. Missing the required [adapter] table entirely.
cat > "$work/m.toml" <<'EOF'
[strings]
adapter_label = "x"
EOF
expect_reject "missing [adapter]"

# 3. [adapter] missing a required field (min_spt_core_version).
cat > "$work/m.toml" <<'EOF'
[adapter]
name = "claude-spt"
version = "0.1.0"
EOF
expect_reject "missing required adapter.min_spt_core_version"

# 4. Bad enum value for adapter.kind.
cat > "$work/m.toml" <<'EOF'
[adapter]
name = "claude-spt"
kind = "robot"
version = "0.1.0"
min_spt_core_version = "0.7.0"
EOF
expect_reject "invalid adapter.kind enum"

# 5. Wrong type: a hook with no `fires` (required) / wrong shape.
cat > "$work/m.toml" <<'EOF'
[adapter]
name = "claude-spt"
version = "0.1.0"
min_spt_core_version = "0.7.0"
[hooks.SessionStart]
reads = ["session_id"]
EOF
expect_reject "hook missing required `fires`"

# 6. Wrong type: shortcut_basename as an integer.
cat > "$work/m.toml" <<'EOF'
[adapter]
name = "claude-spt"
version = "0.1.0"
min_spt_core_version = "0.7.0"
shortcut_basename = 42
EOF
expect_reject "non-string shortcut_basename"

[ "$fail" -eq 0 ] && { echo "MANIFEST-SCHEMA OK"; exit 0; } || { echo "MANIFEST-SCHEMA FAIL"; exit 1; }


Wire the gate into run-gates.sh (replace SKIP stub).


Edit

gate "manifest-schema"
echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"

gate "manifest-schema"
if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
else
  echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"
fi


The file C:\Users\decid\Documents\projects\spt-claude-code\ci\run-gates.sh has been updated successfully. (file state is current in your context — no need to Read it back)

gate "manifest-schema"
if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
else
  echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"
fi

#!/bin/sh
# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.
# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]
# [impl->REQ-CI-MANUAL]
# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is
# always announced so "green" never hides un-run coverage).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
cd "$ROOT" || exit 2
rc=0
gate() { printf '\n=== GATE: %s ===\n' "$1"; }
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

gate "shell-syntax (sh -n)"
for f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do
  if sh -n "$f"; then printf 'ok  %s\n' "$f"; else fail "syntax: $f"; fi
done

gate "unit tests (tests/*.sh)"
for t in tests/*.sh; do
  [ -e "$t" ] || continue
  if sh "$t" >/dev/null 2>&1; then printf 'ok  %s\n' "$t"; else fail "unit: $t"; sh "$t" || true; fi
done

gate "traceable-reqs check (requirement coverage)"
if command -v traceable-reqs >/dev/null 2>&1; then
  if traceable-reqs check >/dev/null 2>&1; then echo "ok  coverage green"; else fail "traceable-reqs check"; traceable-reqs check || true; fi
else
  echo "SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)"
fi

gate "skeleton-validate (cplugs installability)"
if [ -x "$ROOT/ci/publish/validate-skeleton.sh" ] || [ -f "$ROOT/ci/publish/validate-skeleton.sh" ]; then
  if sh "$ROOT/ci/publish/validate-skeleton.sh" >/dev/null 2>&1; then echo "ok  skeleton installable"; else fail "skeleton-validate"; sh "$ROOT/ci/publish/validate-skeleton.sh" || true; fi
else
  echo "SKIP: no skeleton validator yet"
fi

gate "manifest-schema"
echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"

gate "docs-drift"
if [ -f "$ROOT/ci/docs/check-docs.sh" ]; then
  if sh "$ROOT/ci/docs/check-docs.sh"; then :; else fail "docs-drift"; fi
else
  echo "SKIP: no docs check yet"
fi

printf '\n=== RESULT: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
exit "$rc"

-echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"

+if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then

+  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi

+  echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"


Now traceability: add scan roots + activate the REQ.


roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]

roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "adapter", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]


The file C:\Users\decid\Documents\projects\spt-claude-code\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

# traceable-reqs manifest for spt-claude-code — SEED.
#
# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
#
# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
#
# ACQUIRE the CLI (it is a released binary, not a crate dependency):
#   gh release download --repo BigscreenVR/traceable-reqs \
#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
# the installed CLI before relying on it: `traceable-reqs check`.
#
# THE FOUR-STAGE CONTRACT: every requirement is traced from
#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
# Tag evidence in the SAME commit, on/above the real evidence:
#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
# `required_stages` when work on them begins. Deferred items stay [] until promoted.
# Never declare the full end-state stage list up front — activate incrementally as evidence
# lands (add "int" at the final integrating wave).

[scan]
# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
# nonexistent root is a hard manifest_error, so only list what is on disk now.
roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]

[policy]
# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
# of the coverage gate.
required_stages = []

# ───────────────────────────── Requirements ─────────────────────────────────
# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
#
# [[requirements]]
# id = "REQ-EXAMPLE-1"
# title = "One-line statement of the requirement"
# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
#
# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
# each requiring at least "unit" (add "int" where cross-process/cross-node):
#
# [[requirements]]
# id = "REQ-HAZARD-EXAMPLE"
# title = "The invariant, stated so a test can prove it"
# required_stages = []

# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not
# the whole registry up front). All inactive (required_stages = []) so `check` stays
# green until body #2 lands evidence and flips each to ["doc"] / ["doc","impl"] etc.
# Seeded from SCOPE.md "Parity-trim" (Distribution + ADD buckets).

[[requirements]]
id = "REQ-DIST-PLUGIN-SKELETON"
title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).

[[requirements]]
id = "REQ-DIST-BOOTSTRAP-INSTALL"
title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.

[[requirements]]
id = "REQ-DIST-HOOKS-API"
title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.

[[requirements]]
id = "REQ-DIST-MANIFEST-SCHEMA"
title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
required_stages = []

[[requirements]]
id = "REQ-UPS-INJECTION"
title = "UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0002. impl: user-prompt-submit.sh. unit: render_frames test. int (UPS-fires + multi-frame post-F-002) held for throwaway validation.

# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +
# spt reporting bus. Seeded from SCOPE.md "CI model" + docs/CI.md. Real-claude acceptance is a
# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.

[[requirements]]
id = "REQ-CI-GATES"
title = "Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command"
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CI-MANUAL"
title = "A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-TRIGGER"
title = "A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-BUS"
title = "CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-OWL-DISCOVERY"
title = "The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path"
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CI-ACCEPTANCE"
title = "Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)"
required_stages = ["doc", "impl", "int"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).

[[requirements]]
id = "REQ-HAZARD-PERCH-COLLISION"
title = "The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)"
required_stages = ["unit"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under "perri" and displaced the live poll stream.

[[requirements]]
id = "REQ-HAZARD-MSYS-PATHCONV"
title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).

# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs
# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.

[[requirements]]
id = "REQ-DOCS-SITE"
title = "docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)"
required_stages = ["doc", "impl"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.

[[requirements]]
id = "REQ-DOCS-DRIFT"
title = "Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build"
required_stages = ["doc", "impl", "unit"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.

[[requirements]]
id = "REQ-SKILL-VERSION"
title = "/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)"
required_stages = []

-roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]

+roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "adapter", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]


id = "REQ-DIST-MANIFEST-SCHEMA"
title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
required_stages = []

id = "REQ-DIST-MANIFEST-SCHEMA"
title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int held: real `spt adapter add` registration-validation (2nd layer) for throwaway validation.


id = "REQ-DIST-MANIFEST-SCHEMA"
title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int held: real `spt adapter add` registration-validation (2nd layer) for throwaway validation.

# traceable-reqs manifest for spt-claude-code — SEED.
#
# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
#
# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
#
# ACQUIRE the CLI (it is a released binary, not a crate dependency):
#   gh release download --repo BigscreenVR/traceable-reqs \
#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
# the installed CLI before relying on it: `traceable-reqs check`.
#
# THE FOUR-STAGE CONTRACT: every requirement is traced from
#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
# Tag evidence in the SAME commit, on/above the real evidence:
#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
# `required_stages` when work on them begins. Deferred items stay [] until promoted.
# Never declare the full end-state stage list up front — activate incrementally as evidence
# lands (add "int" at the final integrating wave).

[scan]
# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
# nonexistent root is a hard manifest_error, so only list what is on disk now.
roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "adapter", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]

[policy]
# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
# of the coverage gate.
required_stages = []

# ───────────────────────────── Requirements ─────────────────────────────────
# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
#
# [[requirements]]
# id = "REQ-EXAMPLE-1"
# title = "One-line statement of the requirement"
# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
#
# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
# each requiring at least "unit" (add "int" where cross-process/cross-node):
#
# [[requirements]]
# id = "REQ-HAZARD-EXAMPLE"
# title = "The invariant, stated so a test can prove it"
# required_stages = []

# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not
# the whole registry up front). All inactive (required_stages = []) so `check` stays
# green until body #2 lands evidence and flips each to ["doc"] / ["doc","impl"] etc.
# Seeded from SCOPE.md "Parity-trim" (Distribution + ADD buckets).

[[requirements]]
id = "REQ-DIST-PLUGIN-SKELETON"
title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).

[[requirements]]
id = "REQ-DIST-BOOTSTRAP-INSTALL"
title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.

[[requirements]]
id = "REQ-DIST-HOOKS-API"
title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.

[[requirements]]
id = "REQ-DIST-MANIFEST-SCHEMA"
title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
required_stages = []

[[requirements]]
id = "REQ-UPS-INJECTION"
title = "UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0002. impl: user-prompt-submit.sh. unit: render_frames test. int (UPS-fires + multi-frame post-F-002) held for throwaway validation.

# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +
# spt reporting bus. Seeded from SCOPE.md "CI model" + docs/CI.md. Real-claude acceptance is a
# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.

[[requirements]]
id = "REQ-CI-GATES"
title = "Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command"
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CI-MANUAL"
title = "A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-TRIGGER"
title = "A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-BUS"
title = "CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-OWL-DISCOVERY"
title = "The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path"
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CI-ACCEPTANCE"
title = "Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)"
required_stages = ["doc", "impl", "int"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).

[[requirements]]
id = "REQ-HAZARD-PERCH-COLLISION"
title = "The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)"
required_stages = ["unit"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under "perri" and displaced the live poll stream.

[[requirements]]
id = "REQ-HAZARD-MSYS-PATHCONV"
title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).

# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs
# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.

[[requirements]]
id = "REQ-DOCS-SITE"
title = "docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)"
required_stages = ["doc", "impl"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.

[[requirements]]
id = "REQ-DOCS-DRIFT"
title = "Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build"
required_stages = ["doc", "impl", "unit"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.

[[requirements]]
id = "REQ-SKILL-VERSION"
title = "/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)"
required_stages = []

+required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int held: real `spt adapter add` registration-validation (2nd layer) for throwaway validation.


Add the doc-stage tag in ADR-0001, on the manifest version-of-truth evidence.


## Consequences

- The marketplace plugin can sit static across many manifest/binary updates → the
  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /
  `/spt:version`), **not** the marketplace `plugin.json` version.

## Consequences

<!-- [doc->REQ-DIST-MANIFEST-SCHEMA] -->
- The marketplace plugin can sit static across many manifest/binary updates → the
  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /
  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest
  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`
  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`).