    #[test] // [unit->REQ-MANIFEST-1]
    fn syntactically_broken_toml_is_an_error_not_a_panic() {
        let e = Manifest::from_toml_str("this is not = = toml").unwrap_err();
        assert!(matches!(e, ManifestError::Parse(_)));
    }
}

    #[test] // [unit->REQ-MANIFEST-1]
    fn syntactically_broken_toml_is_an_error_not_a_panic() {
        let e = Manifest::from_toml_str("this is not = = toml").unwrap_err();
        assert!(matches!(e, ManifestError::Parse(_)));
    }

    // ---- M6-D3: the published manifest JSON Schema ----

    #[test] // [unit->REQ-DOCS-4] the generated schema carries the stable $id at
    // the canonical Pages URL (ADR-0014), the adapter header's requireds, and
    // the closed enum vocabularies (the things adapter authors validate against).
    fn manifest_schema_shape() {
        let s = manifest_json_schema();
        let v: serde_json::Value = serde_json::from_str(&s).expect("schema is valid JSON");
        assert_eq!(
            v["$id"],
            "https://sabermage.github.io/spt-releases/manifest.schema.json"
        );
        assert_eq!(v["title"], "spt-core runtime manifest");
        // [adapter] is the only required top-level section.
        let req: Vec<&str> = v["required"]
            .as_array()
            .unwrap()
            .iter()
            .map(|x| x.as_str().unwrap())
            .collect();
        assert_eq!(req, ["adapter"], "only the header is mandatory");
        // The header's own requireds include the compat gate.
        let adapter_req = v["$defs"]["Adapter"]["required"].as_array().unwrap();
        assert!(adapter_req.iter().any(|x| x == "min_spt_core_version"));
        // Closed vocabularies surface as enums (e.g. the update avenues).
        let avenues = serde_json::to_string(&v["$defs"]["UpdateAvenue"]).unwrap();
        assert!(avenues.contains("file_pull") && avenues.contains("delegated"));
        // Doc comments ride along as descriptions (the schema IS documentation).
        assert!(s.contains("opaque"), "descriptions must carry the contract's language");
    }

    /// The drift gate (M6-D3): the checked-in `manifest.schema.json` — the
    /// copy that ships to the docs site root and the release assets — must
    /// equal what the current derives generate. CI runs this in the normal
    /// sweep on every push, so the published schema can never lag the parser.
    /// Regenerate: `SPT_BLESS=1 cargo test -p spt-runtime manifest_schema`.
    // [int->REQ-DOCS-5]
    #[test]
    fn checked_in_schema_is_current() {
        let want = manifest_json_schema();
        let path =
            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("manifest.schema.json");
        if std::env::var("SPT_BLESS").ok().as_deref() == Some("1") {
            std::fs::write(&path, &want).expect("bless schema");
            return;
        }
        let got = std::fs::read_to_string(&path)
            .expect("checked-in manifest.schema.json (regenerate: SPT_BLESS=1 cargo test -p spt-runtime)");
        assert_eq!(
            got.replace("\r\n", "\n"),
            want,
            "manifest.schema.json drifted from the derives — regenerate with \
             SPT_BLESS=1 cargo test -p spt-runtime"
        );
    }
}

+    // ---- M6-D3: the published manifest JSON Schema ----

+    #[test] // [unit->REQ-DOCS-4] the generated schema carries the stable $id at

+    // the canonical Pages URL (ADR-0014), the adapter header's requireds, and

+    // the closed enum vocabularies (the things adapter authors validate against).

+        let s = manifest_json_schema();

+        let v: serde_json::Value = serde_json::from_str(&s).expect("schema is valid JSON");

+            "https://sabermage.github.io/spt-releases/manifest.schema.json"

+        assert_eq!(v["title"], "spt-core runtime manifest");

+        // [adapter] is the only required top-level section.

+        let req: Vec<&str> = v["required"]

+            .map(|x| x.as_str().unwrap())

+        assert_eq!(req, ["adapter"], "only the header is mandatory");

+        // The header's own requireds include the compat gate.

+        let adapter_req = v["$defs"]["Adapter"]["required"].as_array().unwrap();

+        assert!(adapter_req.iter().any(|x| x == "min_spt_core_version"));

+        // Closed vocabularies surface as enums (e.g. the update avenues).

+        let avenues = serde_json::to_string(&v["$defs"]["UpdateAvenue"]).unwrap();

+        assert!(avenues.contains("file_pull") && avenues.contains("delegated"));

+        // Doc comments ride along as descriptions (the schema IS documentation).

+        assert!(s.contains("opaque"), "descriptions must carry the contract's language");

+    /// The drift gate (M6-D3): the checked-in `manifest.schema.json` — the

+    /// copy that ships to the docs site root and the release assets — must

+    /// equal what the current derives generate. CI runs this in the normal

+    /// sweep on every push, so the published schema can never lag the parser.

+    /// Regenerate: `SPT_BLESS=1 cargo test -p spt-runtime manifest_schema`.

+    fn checked_in_schema_is_current() {

+        let want = manifest_json_schema();

+            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("manifest.schema.json");

+        if std::env::var("SPT_BLESS").ok().as_deref() == Some("1") {

+            std::fs::write(&path, &want).expect("bless schema");

+        let got = std::fs::read_to_string(&path)

+            .expect("checked-in manifest.schema.json (regenerate: SPT_BLESS=1 cargo test -p spt-runtime)");

+            "manifest.schema.json drifted from the derives — regenerate with \

+             SPT_BLESS=1 cargo test -p spt-runtime"


Edit

/// requirements) are enforced by spt-core's validate step beyond this
/// schema.
// [impl->REQ-DOCS-5]

/// requirements) are enforced by spt-core's validate step beyond this
/// schema.
// [impl->REQ-DOCS-4]
// [impl->REQ-DOCS-5]


<tool_use_error>String to replace not found in file.
String: /// requirements) are enforced by spt-core's validate step beyond this
/// schema.
// [impl->REQ-DOCS-5]</tool_use_error>

Error: String to replace not found in file.
String: /// requirements) are enforced by spt-core's validate step beyond this
/// schema.
// [impl->REQ-DOCS-5]


519-/// Schema validates the equivalent JSON document — what `spt adapter add`'s
520-/// validate-first step checks after TOML parse). Cross-field invariants
521-\ ([`Manifest::validate`]) are enforced by spt-core, not expressible here.
522:// [impl->REQ-DOCS-5]


505	    fn from(e: toml::de::Error) -> Self {
506	        ManifestError::Parse(e)
507	    }
508	}
509
510	/// The published manifest JSON Schema (M6-D3, REQ-DOCS-4): generated from the
511	/// **same derives that parse adapter manifests**, so the published contract
512	/// structurally cannot drift from the parser. Carries a stable `$id` at the
513	/// canonical Pages URL (ADR-0014). The checked-in copy
514	/// (`crates/spt-runtime/manifest.schema.json`) is the site/release asset;
515	/// the `checked_in_schema_is_current` test is the drift gate (regenerate with
516	/// `SPT_BLESS=1 cargo test -p spt-runtime`).
517	///
518	/// The schema documents the *data model*; the on-disk format is TOML (JSON
519	/// Schema validates the equivalent JSON document — what `spt adapter add`'s
520	/// validate-first step checks after TOML parse). Cross-field invariants
521	/// ([`Manifest::validate`]) are enforced by spt-core, not expressible here.
522	// [impl->REQ-DOCS-5]
523	pub fn manifest_json_schema() -> String {
524	    let schema = schemars::schema_for!(Manifest);
525	    let mut v = serde_json::to_value(&schema).expect("schema is JSON");
526	    let obj = v.as_object_mut().expect("root schema is an object");
527	    obj.insert(
528	        "$id".into(),
529	        "https://sabermage.github.io/spt-releases/manifest.schema.json".into(),
530	    );
531	    obj.insert("title".into(), "spt-core runtime manifest".into());
532	    obj.insert(
533	        "description".into(),
534	        "Per-adapter runtime manifest for the spt-core harness contract. \
535	         Authored as TOML (this schema describes the equivalent data model). \
536	         A manifest declares only what varies per harness/shell; command \
537	         templates are opaque strings spt-core never parses. Cross-field \
538	         invariants (kind<->[shell] agreement, strategy/avenue field \
539	         requirements) are enforced by spt-core's validate step beyond this \
540	         schema."
541	            .into(),
542	    );
543	    let mut s = serde_json::to_string_pretty(&v).expect("schema serializes");
544	    s.push('\n');
545	    s
546	}
547
548	impl Manifest {
549	    /// Parse a manifest from TOML text, then validate its cross-field
550	    /// invariants. Never panics on adapter input — malformed TOML, an unknown
551	    /// enum variant, or a broken invariant all return [`ManifestError`].
552	    pub fn from_toml_str(s: &str) -> Result<Manifest, ManifestError> {
553	        let manifest: Manifest = toml::from_str(s)?;
554	        manifest.validate()?;

    fn from(e: toml::de::Error) -> Self {
        ManifestError::Parse(e)
    }
}

/// The published manifest JSON Schema (M6-D3, REQ-DOCS-4): generated from the
/// **same derives that parse adapter manifests**, so the published contract
/// structurally cannot drift from the parser. Carries a stable `$id` at the
/// canonical Pages URL (ADR-0014). The checked-in copy
/// (`crates/spt-runtime/manifest.schema.json`) is the site/release asset;
/// the `checked_in_schema_is_current` test is the drift gate (regenerate with
/// `SPT_BLESS=1 cargo test -p spt-runtime`).
///
/// The schema documents the *data model*; the on-disk format is TOML (JSON
/// Schema validates the equivalent JSON document — what `spt adapter add`'s
/// validate-first step checks after TOML parse). Cross-field invariants
/// ([`Manifest::validate`]) are enforced by spt-core, not expressible here.
// [impl->REQ-DOCS-5]
pub fn manifest_json_schema() -> String {
    let schema = schemars::schema_for!(Manifest);
    let mut v = serde_json::to_value(&schema).expect("schema is JSON");
    let obj = v.as_object_mut().expect("root schema is an object");
    obj.insert(
        "$id".into(),
        "https://sabermage.github.io/spt-releases/manifest.schema.json".into(),
    );
    obj.insert("title".into(), "spt-core runtime manifest".into());
    obj.insert(
        "description".into(),
        "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."
            .into(),
    );
    let mut s = serde_json::to_string_pretty(&v).expect("schema serializes");
    s.push('\n');
    s
}

impl Manifest {
    /// Parse a manifest from TOML text, then validate its cross-field
    /// invariants. Never panics on adapter input — malformed TOML, an unknown
    /// enum variant, or a broken invariant all return [`ManifestError`].
    pub fn from_toml_str(s: &str) -> Result<Manifest, ManifestError> {
        let manifest: Manifest = toml::from_str(s)?;
        manifest.validate()?;


/// ([`Manifest::validate`]) are enforced by spt-core, not expressible here.
// [impl->REQ-DOCS-5]
pub fn manifest_json_schema() -> String {

/// ([`Manifest::validate`]) are enforced by spt-core, not expressible here.
// [impl->REQ-DOCS-4]
// [impl->REQ-DOCS-5]
pub fn manifest_json_schema() -> String {


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\manifest.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// ([`Manifest::validate`]) are enforced by spt-core, not expressible here.
// [impl->REQ-DOCS-5]
pub fn manifest_json_schema() -> String {

/// ([`Manifest::validate`]) are enforced by spt-core, not expressible here.
// [impl->REQ-DOCS-4]
// [impl->REQ-DOCS-5]
pub fn manifest_json_schema() -> String {

/// Schema validates the equivalent JSON document — what `spt adapter add`'s

/// validate-first step checks after TOML parse). Cross-field invariants

let schema = schemars::schema_for!(Manifest);


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; $env:SPT_BLESS="1"; cargo test -p spt-runtime manifest_schema 2>&1 | Select-Object -Last 8; Remove-Item Env:SPT_BLESS

Build with schemars + bless schema file


    Finished `test` profile [unoptimized + debuginfo] target(s) in 8.89s
     Running unittests src\lib.rs (target\debug\deps\spt_runtime-ad7ce3751304cdc8.exe)

running 1 test
test manifest::tests::manifest_schema_shape ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.00s


cd C:\Users\decid\Documents\projects\spt-core; $env:SPT_BLESS="1"; cargo test -p spt-runtime checked_in_schema 2>&1 | Select-Object -Last 5; Remove-Item Env:SPT_BLESS; $env:SPT_BLESS=$null; cargo test -p spt-runtime 2>&1 | Select-String "test result"; Get-Item crates\spt-runtime\manifest.schema.json | Select-Object Length

Bless schema then verify full suite green


running 1 test
test manifest::tests::checked_in_schema_is_current ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 25 filtered out; finished in 0.00s

test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Length : 22063


1	{
2	  "$schema": "https://json-schema.org/draft/2020-12/schema",
3	  "title": "spt-core runtime manifest",
4	  "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.",
5	  "type": "object",
6	  "properties": {
7	    "adapter": {
8	      "$ref": "#/$defs/Adapter"
9	    },
10	    "hooks": {
11	      "description": "`[hooks.<event>]` — inbound hook table, keyed by harness event name.",
12	      "type": "object",
13	      "additionalProperties": {
14	        "$ref": "#/$defs/Hook"
15	      }
16	    },
17	    "session": {
18	      "description": "`[session]` — watched-dir keys plus the `[session.<role>]` templates.",
19	      "$ref": "#/$defs/Session"
20	    },
21	    "env": {
22	      "description": "`[env.<VAR>]` — env-var inject/read table.",
23	      "type": "object",
24	      "additionalProperties": {
25	        "$ref": "#/$defs/EnvVar"
26	      }
27	    },
28	    "history": {
29	      "anyOf": [
30	        {
31	          "$ref": "#/$defs/History"
32	        },
33	        {
34	          "type": "null"
35	        }
36	      ]
37	    },
38	    "inject": {
39	      "anyOf": [
40	        {
41	          "$ref": "#/$defs/Inject"
42	        },
43	        {
44	          "type": "null"
45	        }
46	      ]
47	    },
48	    "identity": {
49	      "anyOf": [
50	        {
51	          "$ref": "#/$defs/Identity"
52	        },
53	        {
54	          "type": "null"
55	        }
56	      ]
57	    },
58	    "update": {
59	      "anyOf": [
60	        {

{
  "$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"
        }
      ]
    },
    "inject": {
      "anyOf": [
        {
          "$ref": "#/$defs/Inject"
        },
        {
          "type": "null"
        }
      ]
    },
    "identity": {
      "anyOf": [
        {
          "$ref": "#/$defs/Identity"
        },
        {
          "type": "null"
        }
      ]
    },
    "update": {
      "anyOf": [
        {


Edit

id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)

id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = ["doc", "impl", "unit"]  # activated M6-D3 (schema leg): manifest_json_schema() generated from the SAME schemars derives that parse adapter manifests (drift impossible by construction), stable $id at the canonical Pages URL, checked-in crates/spt-runtime/manifest.schema.json = the site/release asset. llms.txt + CLI-ref legs land D5; MCP doc server + `--help --json` EXCLUDED → docs/DEFERRED.md (Tier 2, DOCS-STRATEGY §v0.1)


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)

id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = ["impl", "int"]  # activated M6-D3 (schema gate): checked_in_schema_is_current — the checked-in asset must equal what the live derives generate, run in the normal CI sweep every push (regenerate via SPT_BLESS=1). CLI-ref + llms-export drift gates join at D5 through the same pattern


## Agent-consumable docs (spt-core specifics)

## Agent-consumable docs (spt-core specifics)

<!-- [doc->REQ-DOCS-4] the agent-consumable layer's design; the schema leg shipped M6-D3 (schemars-derived, $id at the canonical URL), llms/CLI legs M6-D5, MCP + --json deferred (DEFERRED.md) -->


The file C:\Users\decid\Documents\projects\spt-core\docs\DOCS-STRATEGY.md has been updated successfully. (file state is current in your context — no need to Read it back)

## Agent-consumable docs (spt-core specifics)

<!-- [doc->REQ-DOCS-4] the agent-consumable layer's design; the schema leg shipped M6-D3 (schemars-derived, $id at the canonical URL), llms/CLI legs M6-D5, MCP + --json deferred (DEFERRED.md) -->

+<!-- [doc->REQ-DOCS-4] the agent-consumable layer's design; the schema leg shipped M6-D3 (schemars-derived, $id at the canonical URL), llms/CLI legs M6-D5, MCP + --json deferred (DEFERRED.md) -->

- **`llms.txt` / `llms-full.txt`** — auto-emitted in CI; the slim index lets a tool answer quick questions, the full export feeds deep ingestion.

- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry the canonical path and drop human-only narrative.

- **Machine-readable manifest/contract schema** (JSON Schema) at a stable, discoverable path — agents validate their adapter manifests against it; the schema *is* documentation.


| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |

| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |


The file C:\Users\decid\Documents\projects\spt-core\docs\DEFERRED.md has been updated successfully. (file state is current in your context — no need to Read it back)

| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |
| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |

# Deferred Features

Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one MRA API (`spt-daemon::presence`). A future PresenceChannel's dispatch/bind/thread styles consume exactly this datum plus the broker PresenceLog seam (REQ-EP-4); nothing about the endpoint type needs a new gossip channel. | Once Shells exist and the subnet registry is stable |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). <!-- [doc->REQ-INST-6] --> **M5-D5b note:** remote `spt suspend/wake <id@node>` shipped (the rest-op wire family, remote-drive trust class) — it *moves existing instances between rest states only*; the remote-**fork** arm (creating an instance on a node that has none) stays here, riding instantiate-anywhere's consent gate. A wake addressed to a node with no local instance refuses with `WAKE_NO_REACHABLE_INSTANCE` naming this deferral. | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |
| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |
| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |
| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the GUI milestone's background expectations) |

+| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concrete MCP-resource need) |


M6-D1: spt-releases repo + public face (README, licenses)

Create SaberMage/spt-releases private via gh; author README.md per grill decision 7; LICENSE-MIT + LICENSE-BINARY; Pages config deferred to D7 flip. Activate REQ-REL-1 doc stage.

Creating spt-releases repo + public face

M6-D2: installer scripts (install.sh / install.ps1)

Non-interactive: latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → print absolute path. Idempotent. CI rung against staged artifact + spt --version. Activate REQ-INSTALL-1/2/3/5.

M6-D3: manifest JSON Schema (schemars) + drift gate

schemars derives on spt-runtime manifest types; emit manifest.schema.json at stable site-root path + release asset; CI regenerate+diff gate. Activate REQ-DOCS-4 (partial), REQ-DOCS-5.

M6-D4: docs-site Tier 1 content (mdBook)

mdBook skeleton + Starlight-inspired theme; mental model; human quickstart; adapter quickstart (mock-adapter); harness-contract vertical complete; shell getting-started (spt-shell-notify); stubs for remaining verticals. Activate REQ-DOCS-1/2/3.

M6-D5: agent-export + generation plumbing

llms.txt + llms-full.txt; raw .md alongside HTML; CLI ref from clap; rustdoc → Pages; source-repo CI publish job → spt-releases Pages branch; drift gates. REQ-REL-1 impl, REQ-DOCS-4/5 complete.

M6-D6: release pipeline + signing ceremony

Workspace → 0.1.0; tag-triggered build matrix both runners; cross-repo release via fine-grained token; key ceremony per ADR-0015 (primary + recovery, embed both pubkeys in VerifyPolicy trusted set, seed backups); manual SignedRelease signing. Activate REQ-REL-2/3.

M6-D7: v0.1.0 ship + flip public + acceptance run

Tag v0.1.0; ceremony; publish release; flip spt-releases public; Pages live; acceptance proof per M6-PLAN (clean-machine install, quickstarts, llms.txt + schema fetch, staged v0.1.1 verify, recovery-key verify).


400	required_stages = []  # activates at M6 start (D6)
401
402	# ───────────────────────────── Documentation ────────────────────────────────
403	[[requirements]]
404	id = "REQ-DOCS-1"
405	title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
406	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md D4/D5 — DOCS-STRATEGY §v0.1 Tier 1)
407	[[requirements]]
408	id = "REQ-DOCS-2"
409	title = "Sub-10-minute runnable killer quickstart per audience"
410	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
411	[[requirements]]
412	id = "REQ-DOCS-3"
413	title = "Diátaxis structure; one canonical way to do X"
414	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
415	[[requirements]]
416	id = "REQ-DOCS-4"
417	title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
418	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
419	[[requirements]]
420	id = "REQ-DOCS-5"
421	title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
422	required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
423
424	# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════
425	# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require

required_stages = []  # activates at M6 start (D6)

# ───────────────────────────── Documentation ────────────────────────────────
[[requirements]]
id = "REQ-DOCS-1"
title = "Dual-audience docs (human + AI dev-agent), markdown once / two depths"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md D4/D5 — DOCS-STRATEGY §v0.1 Tier 1)
[[requirements]]
id = "REQ-DOCS-2"
title = "Sub-10-minute runnable killer quickstart per audience"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
[[requirements]]
id = "REQ-DOCS-3"
title = "Diátaxis structure; one canonical way to do X"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
[[requirements]]
id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)
[[requirements]]
id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)

# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════
# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require


id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)

id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = ["doc", "impl", "unit"]  # activated M6-D3 (schema leg): manifest_json_schema() generated from the SAME schemars derives that parse adapter manifests (drift impossible by construction), stable $id at the canonical Pages URL, checked-in crates/spt-runtime/manifest.schema.json = the site/release asset. llms.txt + CLI-ref legs land D5; MCP doc server + `--help --json` EXCLUDED → docs/DEFERRED.md (Tier 2, DOCS-STRATEGY §v0.1)


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

id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)

id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = ["doc", "impl", "unit"]  # activated M6-D3 (schema leg): manifest_json_schema() generated from the SAME schemars derives that parse adapter manifests (drift impossible by construction), stable $id at the canonical Pages URL, checked-in crates/spt-runtime/manifest.schema.json = the site/release asset. llms.txt + CLI-ref legs land D5; MCP doc server + `--help --json` EXCLUDED → docs/DEFERRED.md (Tier 2, DOCS-STRATEGY §v0.1)

-required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)

+required_stages = ["doc", "impl", "unit"]  # activated M6-D3 (schema leg): manifest_json_schema() generated from the SAME schemars derives that parse adapter manifests (drift impossible by construction), stable $id at the canonical Pages URL, checked-in crates/spt-runtime/manifest.schema.json = the site/release asset. llms.txt + CLI-ref legs land D5; MCP doc server + `--help --json` EXCLUDED → docs/DEFERRED.md (Tier 2, DOCS-STRATEGY §v0.1)

title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"


id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)

id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = ["impl", "int"]  # activated M6-D3 (schema gate): checked_in_schema_is_current — the checked-in asset must equal what the live derives generate, run in the normal CI sweep every push (regenerate via SPT_BLESS=1). CLI-ref + llms-export drift gates join at D5 through the same pattern


id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = []  # rule 5: activates at M6 start (M6-PLAN.md; Tier 2 pieces stay deferred)

id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = ["impl", "int"]  # activated M6-D3 (schema gate): checked_in_schema_is_current — the checked-in asset must equal what the live derives generate, run in the normal CI sweep every push (regenerate via SPT_BLESS=1). CLI-ref + llms-export drift gates join at D5 through the same pattern

+required_stages = ["impl", "int"]  # activated M6-D3 (schema gate): checked_in_schema_is_current — the checked-in asset must equal what the live derives generate, run in the normal CI sweep every push (regenerate via SPT_BLESS=1). CLI-ref + llms-export drift gates join at D5 through the same pattern

# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════

# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; traceable-reqs check > $null 2>&1; "check=$LASTEXITCODE"; cargo build --workspace --all-targets 2>&1 | Select-Object -Last 2; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 2

Check + full workspace build + clippy


check=0
   Compiling spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 32.59s
    Checking spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.94s


cd C:\Users\decid\Documents\projects\spt-core; git add crates/spt-runtime Cargo.lock traceable-reqs.toml docs/DOCS-STRATEGY.md docs/DEFERRED.md; git commit -m @'
feat(schema): D3 — manifest JSON Schema from the parsing derives + drift gate

spt-runtime::manifest_json_schema(): schemars derives on every manifest type
(the SAME types that parse adapter TOML, so the published contract cannot
structurally drift from the parser), doc comments riding along as schema
descriptions, stable $id at the canonical Pages URL (ADR-0014). Checked-in
crates/spt-runtime/manifest.schema.json is the site-root + release asset
(D5/D6 consume it verbatim).

Drift gate: checked_in_schema_is_current runs in the normal CI sweep —
checked-in asset must equal what the live derives generate; regenerate with
SPT_BLESS=1 cargo test -p spt-runtime. Shape test pins $id, the
adapter-header requireds (min_spt_core_version), and the closed enum
vocabularies.

REQ-DOCS-4 activated doc/impl/unit (schema leg; llms + CLI ref at D5; MCP +
--help --json -> DEFERRED.md Tier-2 row), REQ-DOCS-5 activated impl/int.
139/139 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@

Commit D3


warning: in the working copy of 'Cargo.lock', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-runtime/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-runtime/src/manifest.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/DEFERRED.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/DOCS-STRATEGY.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-runtime/manifest.schema.json', LF will be replaced by CRLF the next time Git touches it
[dev-freeform be4e46c] feat(schema): D3 — manifest JSON Schema from the parsing derives + drift gate
 7 files changed, 914 insertions(+), 26 deletions(-)
 create mode 100644 crates/spt-runtime/manifest.schema.json


D3 committed. Checking the in-flight CI run (D1+D2 push) before continuing:


cd C:\Users\decid\Documents\projects\spt-core; gh run view 27033872389 --json status,conclusion,jobs -q '{status:.status,conclusion:.conclusion,jobs:[.jobs[]|{name:.name,status:.status,conclusion:.conclusion}]}'

CI run status


Installer rung green both runners. Traceability job failed — inspecting:


cd C:\Users\decid\Documents\projects\spt-core; gh run view 27033872389 --log-failed 2>&1 | Select-Object -Last 30

Traceability job failure log


traceability	Install traceable-reqs	2026-06-05T18:50:56.1750889Z ./traceable-reqs --version
traceability	Install traceable-reqs	2026-06-05T18:50:56.1782848Z shell: /usr/bin/bash -e {0}
traceability	Install traceable-reqs	2026-06-05T18:50:56.1784449Z env:
traceability	Install traceable-reqs	2026-06-05T18:50:56.1787591Z   GH_TOKEN: ***
traceability	Install traceable-reqs	2026-06-05T18:50:56.1788896Z ##[endgroup]
traceability	Install traceable-reqs	2026-06-05T18:50:56.1931821Z Cloning into '.tr-src'...
traceability	Install traceable-reqs	2026-06-05T18:50:57.4696318Z Note: switching to 'aaf234d37b5437b53fe0d7175c5bb0e1318107ac'.
traceability	Install traceable-reqs	2026-06-05T18:50:57.4696902Z
traceability	Install traceable-reqs	2026-06-05T18:50:57.4697249Z You are in 'detached HEAD' state. You can look around, make experimental
traceability	Install traceable-reqs	2026-06-05T18:50:57.4698110Z changes and commit them, and you can discard any commits you make in this
traceability	Install traceable-reqs	2026-06-05T18:50:57.4698994Z state without impacting any branches by switching back to a branch.
traceability	Install traceable-reqs	2026-06-05T18:50:57.4699695Z
traceability	Install traceable-reqs	2026-06-05T18:50:57.4700104Z If you want to create a new branch to retain commits you create, you may
traceability	Install traceable-reqs	2026-06-05T18:50:57.4700919Z do so (now or later) by using -c with the switch command. Example:
traceability	Install traceable-reqs	2026-06-05T18:50:57.4701436Z
traceability	Install traceable-reqs	2026-06-05T18:50:57.4701646Z   git switch -c <new-branch-name>
traceability	Install traceable-reqs	2026-06-05T18:50:57.4701976Z
traceability	Install traceable-reqs	2026-06-05T18:50:57.4702151Z Or undo this operation with:
traceability	Install traceable-reqs	2026-06-05T18:50:57.4703040Z
traceability	Install traceable-reqs	2026-06-05T18:50:57.4703234Z   git switch -
traceability	Install traceable-reqs	2026-06-05T18:50:57.4703462Z
traceability	Install traceable-reqs	2026-06-05T18:50:57.4703860Z Turn off this advice by setting config variable advice.detachedHead to false
traceability	Install traceable-reqs	2026-06-05T18:50:57.4704394Z
traceability	Install traceable-reqs	2026-06-05T18:50:57.5043371Z error: current package believes it's in a workspace when it's not:
traceability	Install traceable-reqs	2026-06-05T18:50:57.5044589Z current:   /home/david/actions-runner/_work/spt-core/spt-core/.tr-src/rust/Cargo.toml
traceability	Install traceable-reqs	2026-06-05T18:50:57.5045506Z workspace: /home/david/actions-runner/_work/spt-core/spt-core/Cargo.toml
traceability	Install traceable-reqs	2026-06-05T18:50:57.5046115Z
traceability	Install traceable-reqs	2026-06-05T18:50:57.5048092Z this may be fixable by adding `.tr-src/rust` to the `workspace.members` array of the manifest located at: /home/david/actions-runner/_work/spt-core/spt-core/Cargo.toml
traceability	Install traceable-reqs	2026-06-05T18:50:57.5050048Z Alternatively, to keep it out of the workspace, add the package to the `workspace.exclude` array, or add an empty `[workspace]` table to the package's manifest.
traceability	Install traceable-reqs	2026-06-05T18:50:57.5080442Z ##[error]Process completed with exit code 101.


Clone landed inside the spt-core workspace — cargo workspace inference. Fix: build in `$RUNNER_TEMP`:


Edit

          WANT=0.1.2
          BIN="$HOME/.local/bin/traceable-reqs"
          if [ -x "$BIN" ] && "$BIN" --version | grep -q " $WANT\$"; then
            ln -sf "$BIN" ./traceable-reqs
          else
            rm -rf .tr-src
            git clone --depth 1 --branch "v$WANT" \
              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" .tr-src
            cargo build --release --manifest-path .tr-src/rust/Cargo.toml
            mkdir -p "$HOME/.local/bin"
            cp .tr-src/rust/target/release/traceable-reqs "$BIN"
            ln -sf "$BIN" ./traceable-reqs
          fi
          ./traceable-reqs --version

          WANT=0.1.2
          BIN="$HOME/.local/bin/traceable-reqs"
          if [ -x "$BIN" ] && "$BIN" --version | grep -q " $WANT\$"; then
            ln -sf "$BIN" ./traceable-reqs
          else
            # Build OUTSIDE the checkout: a clone inside it gets adopted by
            # the spt-core cargo workspace and refuses to build.
            SRC="$RUNNER_TEMP/tr-src"
            rm -rf "$SRC"
            git clone --depth 1 --branch "v$WANT" \
              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" "$SRC"
            cargo build --release --manifest-path "$SRC/rust/Cargo.toml"
            mkdir -p "$HOME/.local/bin"
            cp "$SRC/rust/target/release/traceable-reqs" "$BIN"
            ln -sf "$BIN" ./traceable-reqs
          fi
          ./traceable-reqs --version


The file C:\Users\decid\Documents\projects\spt-core\.github\workflows\ci.yml has been updated successfully. (file state is current in your context — no need to Read it back)

          WANT=0.1.2
          BIN="$HOME/.local/bin/traceable-reqs"
          if [ -x "$BIN" ] && "$BIN" --version | grep -q " $WANT\$"; then
            ln -sf "$BIN" ./traceable-reqs
          else
            rm -rf .tr-src
            git clone --depth 1 --branch "v$WANT" \
              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" .tr-src
            cargo build --release --manifest-path .tr-src/rust/Cargo.toml
            mkdir -p "$HOME/.local/bin"
            cp .tr-src/rust/target/release/traceable-reqs "$BIN"
            ln -sf "$BIN" ./traceable-reqs
          fi
          ./traceable-reqs --version

          WANT=0.1.2
          BIN="$HOME/.local/bin/traceable-reqs"
          if [ -x "$BIN" ] && "$BIN" --version | grep -q " $WANT\$"; then
            ln -sf "$BIN" ./traceable-reqs
          else
            # Build OUTSIDE the checkout: a clone inside it gets adopted by
            # the spt-core cargo workspace and refuses to build.
            SRC="$RUNNER_TEMP/tr-src"
            rm -rf "$SRC"
            git clone --depth 1 --branch "v$WANT" \
              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" "$SRC"
            cargo build --release --manifest-path "$SRC/rust/Cargo.toml"
            mkdir -p "$HOME/.local/bin"
            cp "$SRC/rust/target/release/traceable-reqs" "$BIN"
            ln -sf "$BIN" ./traceable-reqs
          fi
          ./traceable-reqs --version

+            # Build OUTSIDE the checkout: a clone inside it gets adopted by

+            # the spt-core cargo workspace and refuses to build.

-              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" .tr-src

-            cargo build --release --manifest-path .tr-src/rust/Cargo.toml

+              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" "$SRC"

+            cargo build --release --manifest-path "$SRC/rust/Cargo.toml"

-            cp .tr-src/rust/target/release/traceable-reqs "$BIN"

+            cp "$SRC/rust/target/release/traceable-reqs" "$BIN"


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; git add .github/workflows/ci.yml; git commit -m @'
ci: build the pinned traceable-reqs outside the checkout (RUNNER_TEMP)

A clone inside the checkout gets adopted by the spt-core cargo workspace
("current package believes it is in a workspace") and refuses to build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@; git push 2>&1 | Select-Object -Last 1

Fix + push (D3 rides along)


warning: in the working copy of '.github/workflows/ci.yml', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 9510d9b] ci: build the pinned traceable-reqs outside the checkout (RUNNER_TEMP)
 1 file changed, 7 insertions(+), 4 deletions(-)
   f8e0f8a..9510d9b  dev-freeform -> dev-freeform


cd C:\Users\decid\Documents\projects\spt-core; .\target\debug\spt.exe --help 2>&1; "---"; (Get-Content docs\MANIFEST.md | Measure-Object -Line).Lines; Get-ChildItem adapters\mock -Recurse | Select-Object FullName

CLI surface + MANIFEST size + mock adapter layout


spt-core local messaging

Usage: spt.exe <COMMAND>

Commands:
  send       Send a message (body read from stdin). Fire-and-forget
  ring       Send and block for a reply (body read from stdin; reply printed to stdout)
  ready      Become reachable: register the perch and listen for messages (blocks)
  poll       Receive messages: drain backlog then listen, printing each to stdout
  list       List perches under the owlery (id, state, address, ready, alive)
  stop       Soft-stop a perch: remove the ready marker + unregister (spool preserved)
  whoami     Print this session's own perch id (from $OWL_SESSION_ID / $SPT_AGENT_ID)
  pair       Subnet pairing operations (REQ-PAIR-*). Local-only here — the cross-node ceremony rides spt-net (M4 D4+)
  digest     Show a daemon-hosted session's live activity buffer (PTY digest) — the at-a-glance "what is this agent doing now" view (ADR-0008). Pulls a snapshot, or `--follow`s the delta-stream. Local addressing only (the `[subnet:]id@node` cross-node form is M4)
  rename     Rename an endpoint's logical id, rippled on-disk (REQ-INST-11 / ADR-0006 §2): the Self perch dir, its nested psyche/worker perches, and every `info.json`. Refuses while the perch is live (stop it first). The subnet registry rows ripple when the daemon-held registry lands (D4); the `a-<id>` context branch with the context store (D6)
  access     Endpoint access whitelist (ADR-0009, REQ-SEC-1): which origin nodes may send an endpoint unsolicited off-node inbound. Absent entry = open; `allow` flips the endpoint to restricted; revoking the last node leaves it locked down; `open` deletes the restriction
  grant      Consent grant store (REQ-CONSENT-1): which agents hold which gated capabilities on this node. Default-deny (the access whitelist's opposite polarity) — the innermost ADR-0009 gate. An ungranted ask escalates interactively; `add` is the durable allow-always answer
  adapter    Adapter registration (REQ-INSTALL-4): the node-local registered set under `{SPT_HOME}/adapters/` — what this node can drive/launch (one command for harness and shell adapters). The registered set feeds creation-time adapter selection, shell discovery, and the self-update ripple
  shell      Shell instances (CONTEXT §Shell model, REQ-EP-5): mint, list, and tear down the driven surfaces this agent owns. `spawn` MINTS a new instance identity (`<adapter>-<n>`) — it is not the online switch; bringing an existing offline instance back is `relink` / `persistent` / wake
  notify     Issue a subnet-wide user notification (ADR-0007, REQ-NOTIF-2): produced into the replicated notif spool and first-fired at the user's most-recently-active endpoint in that subnet. Body from the trailing arg, or stdin when omitted
  notif      Inspect and acknowledge notifications (ADR-0007). Dismissal is the explicit ack — it latches and replicates subnet-wide
  update     Self-update operations. `apply` is the explicit ack named by the update-consent notification; it re-verifies the staged release before touching the live daemon
  fork       Fork an endpoint into another subnet as a NEW identity (REQ-INST-15, ADR-0010: home is immutable — fork is the cross-subnet move, never a re-home). Seeds the fork with a one-time copy of the source's mind (live + project tiers); the two diverge immediately (no ongoing sync). The source is untouched unless --delete-source. Same-node only in v1 — the remote arm composes with M5 instantiate-anywhere's consent gate, and the same-name default rides it (one node holds one perch per name, so a local fork needs a new id)
  resources  The subnet resource registry (REQ-INST-14): per-endpoint free-text service blurbs — an agent yellow-pages. `set` authors your own, `show` reads a local endpoint's, `list` projects the registry view over visible rows
  suspend    Rest a local endpoint cold (REQ-INST-3): the resting-state machine's suspend edge. From dormant — or straight from active, in which case the transition echo commune still fires (gate-armed for the endpoint's own pulse loop). Local addressing only — the qualified `id@node` suspend-from-anywhere arm is M5
  wake       Wake a resting local endpoint in place (REQ-INST-3): re-activates the existing seat (state's already there — no fresh spawn), resurfaces undismissed notifs at the Wake boundary, and requests an immediate context freshness pull from trusted peers. Local addressing only
  shutdown   Gracefully shut down an agent's own endpoint (REQ-SHELL-2, CONTEXT §Shell model): soft-stop the listener, then the suspend edge — the transition echo-commune fires (gate-armed) and persistent shells cascade offline. `spt suspend` without the session-over half
  api        Harness-contract inbound surface (REQ-API-1): the entry points a harness's hooks fire to mutate on-disk SPT state
  help       Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version
---
182

FullName
--------
C:\Users\decid\Documents\projects\spt-core\adapters\mock\src
C:\Users\decid\Documents\projects\spt-core\adapters\mock\tests
C:\Users\decid\Documents\projects\spt-core\adapters\mock\Cargo.toml
C:\Users\decid\Documents\projects\spt-core\adapters\mock\manifest.toml
C:\Users\decid\Documents\projects\spt-core\adapters\mock\src\main.rs
C:\Users\decid\Documents\projects\spt-core\adapters\mock\src\shell.rs
C:\Users\decid\Documents\projects\spt-core\adapters\mock\tests\manifest_valid.rs


cd C:\Users\decid\Documents\projects\spt-core; .\target\debug\spt.exe api --help 2>&1

spt api subcommand surface


Harness-contract inbound surface (REQ-API-1): the entry points a harness's hooks fire to mutate on-disk SPT state

Usage: spt.exe api [OPTIONS] --adapter <ADAPTER> <COMMAND>

Commands:
  seed            Harness-hosted startup: record an ephemeral seed keyed by parent pid (T4)
  listen          Consume a seed and hold the perch + relay loop (interim long-runner, T4)
  bind            Post-spawn bind of a session to its perch (T4)
  bind-shell      Shell-binary bind: the type=Shell flavor of `bind` (M5-D3b). Resolves the instance **by link token alone** (the spawn template carries only `{link_token}` — "owner from the link") and flips it online. The credential IS the auth: no token, no bind
  state           Set activity state busy|idle (+ echo-gate sentinel) (T5)
  echo-gate       Manage the echo-gate sentinel directly (T5)
  poll            Drain delivered messages (hook channel) (T5). With `--link` this is the shell-flavored relay drain (M5-D3c): the link token is the auth (CONTEXT §local-link authentication), and the drained rows are the shell's MAC-stamped command/text/file frames
  worker-start    Create a nested worker perch under a parent (T6)
  worker-stop     Tear down a worker perch (T6)
  worker-poll     Drain a worker perch's messages (T6)
  boundary        Rebind the perch to a new session_id, preserving identity (T7)
  session-end     Soft teardown (spool/history preserved); `--erase` hard-wipes (T7)
  presence        Report presence (T7)
  driven-by       Report which node (if any) is remote-driving this endpoint (M4-D5c)
  history-log     Append to the native history store (Path-B) (T7)
  emit            Emit a Shell sensory payload to the owner's **live** session (M5-D3c). REST-only by definition: never spooled, dropped with a diagnostic when the owner isn't live. The link token is the auth
  capability      Print the adapter's declared capability (`hostable_types`) (T7)
  shutdown        Graceful live-agent signoff (M2b): run the final echo-commune BEFORE teardown (3.3), then soft-stop. The `spt shutdown` lifecycle path
  owner-shutdown  A shell suspends its linked owner directly, bypassing agent comms (M5-D4d, CONTEXT §Shell model machinery surface) — gated by the manifest `can_shutdown` pre-consent grant, fail-closed. The firing shell cascades offline with its siblings, by design
  help            Print this message or the help of the given subcommand(s)

Options:
      --adapter <ADAPTER>    adapter_name — the calling harness adapter. Required on every `api` call
      --manifest <MANIFEST>  Path to the adapter's runtime manifest (when the command needs it)
  -h, --help                 Print help


1	# Generic mock harness adapter — the canonical, harness-AGNOSTIC exercise of the
2	# spt-core contract. ZERO vendor harness conventions: no vendor binary name, no
3	# vendor config dir, no vendor hook names. The command templates spawn the
4	# trivial `mock-session` helper; the [hooks.*] table binds generic event names to
5	# the inbound `api` surface.
6	#
7	# This is the only adapter-shaped artifact in spt-core (PRD R-DOCS-2). A real
8	# harness adapter (e.g. the downstream spt-plugin) lives in its own repo.
9
10	[adapter]
11	name = "mock"
12	kind = "harness"
13	version = "1.0.0"
14	min_spt_core_version = "1.0.0"
15	hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
16
17	# Inbound hooks: generic event names → the `api` command each fires. SessionStart
18	# seeds; Idle reports idle (and arms the echo-gate); SessionEnd soft-tears-down.
19	[hooks.SessionStart]
20	fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
21	reads = ["session_id", "parent_pid"]
22	can_inject = true
23
24	[hooks.Idle]
25	fires = "api state idle"
26	reads = []
27	can_inject = false              # no inject channel → sentinel/relay fallback
28
29	[hooks.SessionEnd]
30	fires = "api session-end"
31	reads = []
32	can_inject = false
33
34	# Watched dirs: commune/signoff are file-drops (filenames spt-core-fixed), not
35	# commands.
36	[session]
37	commune_dir = ".mock"
38	signoff_dir = ".mock"
39
40	# Outbound session role: the opaque template ManifestRuntime spawns. `mock-session`
41	# stands in for a real harness binary; spt-core just runs it with keys filled.
42	[session.self]
43	command = "mock-session --id {id} --session-id {session_id}"
44	detach = true
45	keys = ["id", "session_id"]
46
47	# M2b live-agent lifecycle roles (harness-agnostic stand-ins). A real adapter's
48	# templates select a model + harness flags inside `command`; spt-core only fills
49	# {key} placeholders. The recursion guard makes the Psyche/echo children's own
50	# hooks bail (no Psyche-of-Psyche, no echo-of-echo).
51	[session.psyche_init]
52	command = "mock-session --mode psyche --id {id} --session-id {session_id}"
53	cwd = "{psyche_dir}"
54	recursion_guard_env = "MOCK_ECHO_COMMUNE"
55	detach = true
56	keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
57
58	[session.psyche_resume]
59	command = "mock-session --mode psyche --id {id} --session-id {session_id}"
60	cwd = "{psyche_dir}"
61	recursion_guard_env = "MOCK_ECHO_COMMUNE"
62	detach = true
63	keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
64
65	# Echo-commune: a bounded cheap-model template reading history on stdin.
66	[session.echo_commune]
67	command = "mock-session --mode echo --id {id}"
68	recursion_guard_env = "MOCK_ECHO_COMMUNE"
69	keys = ["id"]
70
71	# Signoff: a psyche_resume variant firing a final context-save.
72	[session.signoff]
73	command = "mock-session --mode echo --id {id}"
74	recursion_guard_env = "MOCK_ECHO_COMMUNE"
75	keys = ["id"]
76
77	# History: native Path-B store (the adapter pushes via `api history-log`).
78	[history]
79	strategy = "native"
80
81	# Env aliases the adapter injects for in-session use ($SPT = spt api, etc.).
82	[env.SPT_AGENT_ID]
83	direction = "inject"
84	value = "{id}"
85
86	# Inject methods per activity state (PTY is spt-core/M3, never offered here).
87	[inject]
88	activity = ["hook"]
89	idle = ["hook", "relay"]
90
91	[identity]
92	session_id_source = "post_spawn"
93	parent_ancestor_name = "mock-session"
94

# Generic mock harness adapter — the canonical, harness-AGNOSTIC exercise of the
# spt-core contract. ZERO vendor harness conventions: no vendor binary name, no
# vendor config dir, no vendor hook names. The command templates spawn the
# trivial `mock-session` helper; the [hooks.*] table binds generic event names to
# the inbound `api` surface.
#
# This is the only adapter-shaped artifact in spt-core (PRD R-DOCS-2). A real
# harness adapter (e.g. the downstream spt-plugin) lives in its own repo.

[adapter]
name = "mock"
kind = "harness"
version = "1.0.0"
min_spt_core_version = "1.0.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]

# Inbound hooks: generic event names → the `api` command each fires. SessionStart
# seeds; Idle reports idle (and arms the echo-gate); SessionEnd soft-tears-down.
[hooks.SessionStart]
fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
reads = ["session_id", "parent_pid"]
can_inject = true

[hooks.Idle]
fires = "api state idle"
reads = []
can_inject = false              # no inject channel → sentinel/relay fallback

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

# Watched dirs: commune/signoff are file-drops (filenames spt-core-fixed), not
# commands.
[session]
commune_dir = ".mock"
signoff_dir = ".mock"

# Outbound session role: the opaque template ManifestRuntime spawns. `mock-session`
# stands in for a real harness binary; spt-core just runs it with keys filled.
[session.self]
command = "mock-session --id {id} --session-id {session_id}"
detach = true
keys = ["id", "session_id"]

# M2b live-agent lifecycle roles (harness-agnostic stand-ins). A real adapter's
# templates select a model + harness flags inside `command`; spt-core only fills
# {key} placeholders. The recursion guard makes the Psyche/echo children's own
# hooks bail (no Psyche-of-Psyche, no echo-of-echo).
[session.psyche_init]
command = "mock-session --mode psyche --id {id} --session-id {session_id}"
cwd = "{psyche_dir}"
recursion_guard_env = "MOCK_ECHO_COMMUNE"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

[session.psyche_resume]
command = "mock-session --mode psyche --id {id} --session-id {session_id}"
cwd = "{psyche_dir}"
recursion_guard_env = "MOCK_ECHO_COMMUNE"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

# Echo-commune: a bounded cheap-model template reading history on stdin.
[session.echo_commune]
command = "mock-session --mode echo --id {id}"
recursion_guard_env = "MOCK_ECHO_COMMUNE"
keys = ["id"]

# Signoff: a psyche_resume variant firing a final context-save.
[session.signoff]
command = "mock-session --mode echo --id {id}"
recursion_guard_env = "MOCK_ECHO_COMMUNE"
keys = ["id"]

# History: native Path-B store (the adapter pushes via `api history-log`).
[history]
strategy = "native"

# Env aliases the adapter injects for in-session use ($SPT = spt api, etc.).
[env.SPT_AGENT_ID]
direction = "inject"
value = "{id}"

# Inject methods per activity state (PTY is spt-core/M3, never offered here).
[inject]
activity = ["hook"]
idle = ["hook", "relay"]

[identity]
session_id_source = "post_spawn"
parent_ancestor_name = "mock-session"


1	# Runtime manifest schema
2
3	> Draft design doc (2026-05-31). Resolves PRD §17.2. Implementation detail — see [`../CONTEXT.md`](../CONTEXT.md) for the model. Schema grounded in the sister project's actual functional surface.
4
5	## Principle
6
7	**SPT is not a harness.** The manifest declares only what *varies per harness*. Command templates are **opaque strings** — spt-core never parses out a model, tool list, or flag; the adapter writes the full command line and spt-core just runs it with substitution keys filled. Anything spt-core owns (sentinels, spool/registry/perch schemas, the event-block vocabulary, the version-trampoline mechanism, config knobs) is **not** in the manifest.
8
9	## What is NOT in the manifest (spt-core-owned)
10
11	- **Sentinels** (`.idle`-equiv, echo-gate) — managed by `api state`/`api echo-gate`; the adapter only *calls* them.
12	- **Spool / registry / perch `info.json` / daemon-state schemas** — core.
13	- **Event-block vocabulary** — the XML tags spt-core surfaces to agents (e.g. `<SPT_EVENT>`, `<owl_messages>`-equivalent) are a **fixed, documented constant**, not configurable. Adapters pass spt-core's output through unchanged.
14	- **File-drop filenames** — statically `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md`. Only the *watched directory* is declared (in `[session]`).
15	- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.
16	- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.
17	- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).
18
19	<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the
20	typed form in `crates/spt-runtime/src/manifest.rs` is kept in lockstep. -->
21
22	## Manifest sections
23
24	### `[adapter]` — header
25	```toml
26	[adapter]
27	name = "claude-spt"
28	version = "2.0.0"
29	min_spt_core_version = "1.0.0"     # readable before any update; compat gate
30	hostable_types = ["LiveAgent", "ReadyAgent", "Psyche", "Worker"]
31	```
32
33	### `[hooks.<event>]` — inbound hook table
34	Per harness event: the `api` command it fires, the stdin fields it maps in, and its **output capability** — `can_inject` (whether this hook can surface context to the agent). When `can_inject = false`, spt-core falls back to a sentinel + relay/poll path instead of expecting injection. This is the load-bearing harness-varying fact (CC's Stop hook can't inject).
35
36	```toml
37	[hooks.SessionStart]
38	fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
39	reads = ["session_id", "source", "agent_type"]
40	can_inject = true                  # via additionalContext
41
42	[hooks.Stop]
43	fires = "api state idle"
44	reads = []
45	can_inject = false                 # no hookSpecificOutput schema → sentinel/relay fallback
46	```
47
48	### `[session.<role>]` — outbound session role templates
49	One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.
50
51	```toml
52	[session.psyche_init]
53	command = "claude -p --agents {agents_json} --agent owl-psyche --name {session_name} --model sonnet --fallback-model opus --effort medium --dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write --output-format json"
54	cwd = "{psyche_dir}"
55	env_remove = ["OWL_SESSION_ID"]
56	recursion_guard_env = "SPT_ECHO_COMMUNE"   # set on summarizer children so their own hooks bail
57	detach = true
58	keys = ["agents_json", "session_name", "psyche_dir"]   # substitution keys spt-core fills
59	```
60	(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)
61
62	<!-- [doc->REQ-NOTIF-2] -->
63	`notif` is the **endpoint-native notification render** (ADR-0007's `notif_command` seam): an OS toast, a Shell `alert-symbol`, anything the adapter can run. Declared on harness-adapter **and** shell-adapter manifests; spawned **detached** when a notif surfaces at this endpoint, combinable with the agent-surface delivery (the typed `notify` envelope). Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.
64
65	```toml
66	[session.notif]
67	command = "powershell -Command New-BurntToastNotification -Text '{notif_from}','{notif_body}'"
68	keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
69	```
70
71	### `[env]` — env-var table
72	Vars to inject/read, and **how**. With `spt` on PATH the table is small. The injection *channel* is asymmetric: **spt-hosted** sessions inherit env from the broker that spawns them (no channel needed); **harness-hosted** sessions need the harness's declared channel (the broker didn't spawn them). File-bridge-only-when-not-launcher, applied to env.
73
74	```toml
75	[env.OWL_SESSION_ID]
76	direction = "inject"
77	value = "{session_id}"
78	channel = "CLAUDE_ENV_FILE"        # harness-hosted only; spt-hosted inherits from broker
79	```
80
81	### `[history]` — transcript access
82	Three strategies; pick one. Pull-based (spt-core asks) or push-based (adapter sends).
83	```toml
84	[history]
85	strategy = "fetcher"               # fetcher | locate_normalize | native
86	fetcher = "claude-spt-history --session {session_id}"   # adapter binary emits normalized history
87	# locate_normalize: locate_template + normalize_command
88	# native: adapter pushes via `api history-log`; spt-core stores (also used for Shells)
89	```
90
91	### `[inject]` — inject-input methods
92	Per activity-state, any combination: `pty`, `hook`, `relay`, `http`.
93	```toml
94	[inject]
95	activity = ["hook"]                # non-disruptive during work
96	idle     = ["pty", "hook"]
97	```
98
99	### `[session]` watched dirs (file-drop)
100	```toml
101	[session]
102	commune_dir = ".claude"            # watched for <id>-commune.md (filename fixed by spt-core)
103	signoff_dir = ".claude"            # watched for <id>-signoff.md
104	```
105
106	### `[identity]`
107	```toml
108	[identity]
109	session_id_source = "post_spawn"   # post_spawn | uuid_inject
110	parent_ancestor_name = "claude"    # process-tree anchor when session_id absent
111	```
112
113	### `[pty_digest]` — live activity buffer patterns (ADR-0008)
114
115	<!-- [doc->REQ-TERM-4] The adapter supplies the patterns; spt-core only runs them
116	over the broker PTY byte stream (Path A's "adapter owns the parser" rule, identical
117	to opaque command templates). This is NOT a built-in per-harness parser — it never
118	locates or parses the harness transcript. Absent section ⇒ the digest is simply
119	unavailable for the adapter. -->
120
121	```toml
122	[pty_digest]
123	input_pattern = "\\nUSER> "          # user-input boundary (opens a span until the next match)
124	agent_pattern = "\\nAGENT> "         # agent-output boundary (opens a span until the next match)
125	tool_patterns = [                     # one regex per known tool; name `arg` (+ optional `name`) groups
126	  "(?P<name>Write)\\((?P<arg>[^)]*)\\)",
127	  "(?P<name>Bash)\\((?P<arg>[^)]*)\\)",
128	]
129	catchall_pattern = "(?P<name>[A-Z][a-zA-Z]+)\\((?P<arg>[^)]*)\\)"   # optional: unknowable user tools
130	window_turns = 3                      # optional presentation override (last N user turns); default ~3
131	persist = false                       # opt-in Path-B "Option (C)" coarse activity log; off by default
132	```
133
134	The two boundary markers are **required** when the section is present (an empty
135	boundary degenerates the stream to one undifferentiated span); `tool_patterns`,
136	`catchall_pattern`, and `window_turns` are optional. The patterns are **opaque
137	regex strings** spt-core compiles and runs — model/tool semantics live in the
138	adapter, never spt-core. Presentation knobs other than `window_turns` (tool-arg
139	truncation ~25 chars, ANSI stripping, the rolling scan-buffer cap) are spt-core-owned
140	defaults: the template controls *extraction*, spt-core controls *presentation*.
141
142	Two access modes ride the one R-TERM-3 session surface (raw bytes vs parsed digest
143	= two channels): a **snapshot pull** (`spt digest <id>`) and a **delta-stream**
144	(`spt digest <id> --follow` — only changed turns are pushed). Local addressing only;
145	the qualified `[subnet:]id@node` cross-node form is **M4**.
146
147	### `[update]` — adapter self-update
148	```toml
149	[update]
150	avenue = "delegated"               # delegated | file_pull
151	command = "claude plugin update spt"          # delegated
152	# file_pull: repo + path_regex
153	version_check = true               # verify spt-core satisfies min_spt_core_version before/after
154	uninstall = "claude plugin uninstall spt"     # optional; mirror of install — runs on `adapter remove` once quiesced
155	```
156
157	`spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for `file_pull`; for `delegated`, just deregister and leave the harness's own files).
158
159	---
160
161	## Shell adapters (`kind = "shell"`)
162
163	A second kind of adapter. Provides a `Shell` endpoint (a driven surface) rather than hosting agents. Same file location (`adapters/`), different manifest body.
164
165	```toml
166	[adapter]
167	name = "GameRobot"
168	kind = "shell"                     # vs "harness"
169	version = "1.0.0"
170	min_spt_core_version = "1.0.0"
171
172	[shell]
173	spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
174	ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
175	broadcast = "subnet"               # subnet | same-node | none
176	command_receipt = "http"           # http | stdin | relay   (how it receives agent commands)
177	pre_close = "park-and-save"        # optional instruction sent to the binary on link-break
178	close_timeout_ms = 3000            # graceful termination window before force-close
179	persistent = true                  # auto-online whenever the owner endpoint is online
180	wake_command = "gamerobot-waker --link {link_token}"  # long-running wake-watcher run WHILE offline; exit(wake-opcode) ⇒ revive
181	can_shutdown = false               # if true, the shell may fire `api owner-shutdown` to suspend its owner directly
182	require_approval = "none"          # none | remembered | always — per-spawn user-approval gate (floor; node/endpoint may tighten)
183	max_instances_per_owner = 4        # optional cap on existing instances per owner (online+offline both count); omit ⇒ unlimited
184	over_cap = "reject"                # reject | approve — at the cap: refuse, or require per-spawn approval beyond it (doesn't raise the cap)
185
186	[shell.capabilities]               # the command vocabulary (agent→shell) — the toolset
187	move      = { args = ["direction", "distance"] }
188	gesture   = { args = ["name"] }
189	alert     = { args = ["symbol"] }
190	screenshot = {}
191
192	[shell.sensory]                    # the sensory vocabulary (shell→agent, REST-only)
193	types = ["image", "sound", "event"]   # "event" = arbitrary descriptive payloads
194	```
195
196	The capability + sensory vocabularies live **here**, not on the perch — spt-core resolves them by `adapter_name`. Command-delivery (`command_receipt`) reuses the agent inject-input modes. The shell binary binds via `api bind` (type=Shell, owner from the link) and pushes sensory via `api emit --type <t>`.
197
198	**Sleep/wake:** when offline (+ `wake_command` set), spt-core runs the wake-watcher (mutually exclusive with the shell binary); `exit(wake-opcode)` → wake resolution; crash-exit → respawn with exponential backoff + give-up (until the next relink/launch). **The wake opcode is exit code 86.** The wake-watcher template shares the spawn template's substitution catalog (`{id}`, `{adapter_name}`, `{link_token}`), but its `{link_token}` is minted *unparked* — an offline link has no live credential (the close retired it); a waker wakes by exit code, never by driving the link. `persistent` auto-onlines the shell with its owner. `can_shutdown` authorizes `api owner-shutdown`. Cross-node *fresh-spawn* to wake an owner requires the owner's `shell_wake_spawn_anywhere` settings flag (pre-consent).
199
200	**Instantiation governance:** `require_approval` gates `shell spawn` (reusing the consent grant store — `remembered` persists a grant via allow-always, `always` prompts every spawn); `max_instances_per_owner` + `over_cap` cap how many instances one owner may hold (online + offline both count toward the cap). An instance's **alias** is **not** a manifest field — it is per-instance runtime state on the shell perch's `info.json` (set at `shell spawn --alias` / `shell rename`), since the manifest describes the *adapter*, not individual instances.
201
202	## Worked example: `claude-spt` (today's behavior, expressed)
203
204	Maps the sister project's actual surface onto the schema.
205
206	| Concern | Today (modern SPT) | Manifest expression |
207	|---|---|---|
208	| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |
209	| Psyche init | `claude -p --agents … --model sonnet --fallback-model opus …` | `[session.psyche_init].command` (opaque) |
210	| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
211	| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |
212	| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |
213	| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |
214	| Commune/signoff | `.claude/<id>-commune.md` watched | `[session].commune_dir/signoff_dir`; filenames fixed |
215	| Env | `$OWL`/`$LIVE`/`OWL_SESSION_ID` via `CLAUDE_ENV_FILE` | `[env]`; PATH removes most; channel only harness-hosted |
216	| Identity | `OWL_SESSION_ID` + `claude.exe` ancestor walk | `[identity]` |
217	| Update | (today: cplugs) | `[update] avenue="delegated" command="claude plugin update"` |
218	| period / echo-window / route-guard / 24h refresh | hardcoded/flags | spt-core **global knobs** (per-endpoint override), not manifest |
219
220	## Open sub-items
221	- Exact substitution-key catalog (which keys spt-core guarantees to fill per role).
222	- TOML vs YAML final choice; schema-validation + helpful errors for adapter authors.
223	- The fixed event-block vocabulary's final tag names (`<SPT_EVENT>` etc.).
224	- Capability-declaration shape beyond `hostable_types` (per-type operation advertisement).
225

# Runtime manifest schema

> Draft design doc (2026-05-31). Resolves PRD §17.2. Implementation detail — see [`../CONTEXT.md`](../CONTEXT.md) for the model. Schema grounded in the sister project's actual functional surface.

## Principle

**SPT is not a harness.** The manifest declares only what *varies per harness*. Command templates are **opaque strings** — spt-core never parses out a model, tool list, or flag; the adapter writes the full command line and spt-core just runs it with substitution keys filled. Anything spt-core owns (sentinels, spool/registry/perch schemas, the event-block vocabulary, the version-trampoline mechanism, config knobs) is **not** in the manifest.

## What is NOT in the manifest (spt-core-owned)

- **Sentinels** (`.idle`-equiv, echo-gate) — managed by `api state`/`api echo-gate`; the adapter only *calls* them.
- **Spool / registry / perch `info.json` / daemon-state schemas** — core.
- **Event-block vocabulary** — the XML tags spt-core surfaces to agents (e.g. `<SPT_EVENT>`, `<owl_messages>`-equivalent) are a **fixed, documented constant**, not configurable. Adapters pass spt-core's output through unchanged.
- **File-drop filenames** — statically `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md`. Only the *watched directory* is declared (in `[session]`).
- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.
- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.
- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).

<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the
typed form in `crates/spt-runtime/src/manifest.rs` is kept in lockstep. -->

## Manifest sections

### `[adapter]` — header
```toml
[adapter]
name = "claude-spt"
version = "2.0.0"
min_spt_core_version = "1.0.0"     # readable before any update; compat gate
hostable_types = ["LiveAgent", "ReadyAgent", "Psyche", "Worker"]
```

### `[hooks.<event>]` — inbound hook table
Per harness event: the `api` command it fires, the stdin fields it maps in, and its **output capability** — `can_inject` (whether this hook can surface context to the agent). When `can_inject = false`, spt-core falls back to a sentinel + relay/poll path instead of expecting injection. This is the load-bearing harness-varying fact (CC's Stop hook can't inject).

```toml
[hooks.SessionStart]
fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
reads = ["session_id", "source", "agent_type"]
can_inject = true                  # via additionalContext

[hooks.Stop]
fires = "api state idle"
reads = []
can_inject = false                 # no hookSpecificOutput schema → sentinel/relay fallback
```

### `[session.<role>]` — outbound session role templates
One opaque command template per role: `self`, `psyche_init`, `psyche_resume`, `echo_commune`, `signoff`, `notif`. Plus `cwd`, env inject/remove, detachment, and the recursion-guard env var. **Model, tools, flags, dir-grants all live inside `command`** — not as fields.

```toml
[session.psyche_init]
command = "claude -p --agents {agents_json} --agent owl-psyche --name {session_name} --model sonnet --fallback-model opus --effort medium --dangerously-skip-permissions --disable-slash-commands --tools Read,Edit,Write --output-format json"
cwd = "{psyche_dir}"
env_remove = ["OWL_SESSION_ID"]
recursion_guard_env = "SPT_ECHO_COMMUNE"   # set on summarizer children so their own hooks bail
detach = true
keys = ["agents_json", "session_name", "psyche_dir"]   # substitution keys spt-core fills
```
(`psyche_resume` adds `{session_id}`; both carry the spt-core-owned `{psyche_prompt}`. `signoff` is a `psyche_resume` variant firing a final context-save. `echo_commune` is a cheap-model template consuming history.)

<!-- [doc->REQ-NOTIF-2] -->
`notif` is the **endpoint-native notification render** (ADR-0007's `notif_command` seam): an OS toast, a Shell `alert-symbol`, anything the adapter can run. Declared on harness-adapter **and** shell-adapter manifests; spawned **detached** when a notif surfaces at this endpoint, combinable with the agent-surface delivery (the typed `notify` envelope). Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.

```toml
[session.notif]
command = "powershell -Command New-BurntToastNotification -Text '{notif_from}','{notif_body}'"
keys = ["notif_id", "notif_from", "notif_subnet", "notif_body"]
```

### `[env]` — env-var table
Vars to inject/read, and **how**. With `spt` on PATH the table is small. The injection *channel* is asymmetric: **spt-hosted** sessions inherit env from the broker that spawns them (no channel needed); **harness-hosted** sessions need the harness's declared channel (the broker didn't spawn them). File-bridge-only-when-not-launcher, applied to env.

```toml
[env.OWL_SESSION_ID]
direction = "inject"
value = "{session_id}"
channel = "CLAUDE_ENV_FILE"        # harness-hosted only; spt-hosted inherits from broker
```

### `[history]` — transcript access
Three strategies; pick one. Pull-based (spt-core asks) or push-based (adapter sends).
```toml
[history]
strategy = "fetcher"               # fetcher | locate_normalize | native
fetcher = "claude-spt-history --session {session_id}"   # adapter binary emits normalized history
# locate_normalize: locate_template + normalize_command
# native: adapter pushes via `api history-log`; spt-core stores (also used for Shells)
```

### `[inject]` — inject-input methods
Per activity-state, any combination: `pty`, `hook`, `relay`, `http`.
```toml
[inject]
activity = ["hook"]                # non-disruptive during work
idle     = ["pty", "hook"]
```

### `[session]` watched dirs (file-drop)
```toml
[session]
commune_dir = ".claude"            # watched for <id>-commune.md (filename fixed by spt-core)
signoff_dir = ".claude"            # watched for <id>-signoff.md
```

### `[identity]`
```toml
[identity]
session_id_source = "post_spawn"   # post_spawn | uuid_inject
parent_ancestor_name = "claude"    # process-tree anchor when session_id absent
```

### `[pty_digest]` — live activity buffer patterns (ADR-0008)

<!-- [doc->REQ-TERM-4] The adapter supplies the patterns; spt-core only runs them
over the broker PTY byte stream (Path A's "adapter owns the parser" rule, identical
to opaque command templates). This is NOT a built-in per-harness parser — it never
locates or parses the harness transcript. Absent section ⇒ the digest is simply
unavailable for the adapter. -->

```toml
[pty_digest]
input_pattern = "\\nUSER> "          # user-input boundary (opens a span until the next match)
agent_pattern = "\\nAGENT> "         # agent-output boundary (opens a span until the next match)
tool_patterns = [                     # one regex per known tool; name `arg` (+ optional `name`) groups
  "(?P<name>Write)\\((?P<arg>[^)]*)\\)",
  "(?P<name>Bash)\\((?P<arg>[^)]*)\\)",
]
catchall_pattern = "(?P<name>[A-Z][a-zA-Z]+)\\((?P<arg>[^)]*)\\)"   # optional: unknowable user tools
window_turns = 3                      # optional presentation override (last N user turns); default ~3
persist = false                       # opt-in Path-B "Option (C)" coarse activity log; off by default
```

The two boundary markers are **required** when the section is present (an empty
boundary degenerates the stream to one undifferentiated span); `tool_patterns`,
`catchall_pattern`, and `window_turns` are optional. The patterns are **opaque
regex strings** spt-core compiles and runs — model/tool semantics live in the
adapter, never spt-core. Presentation knobs other than `window_turns` (tool-arg
truncation ~25 chars, ANSI stripping, the rolling scan-buffer cap) are spt-core-owned
defaults: the template controls *extraction*, spt-core controls *presentation*.

Two access modes ride the one R-TERM-3 session surface (raw bytes vs parsed digest
= two channels): a **snapshot pull** (`spt digest <id>`) and a **delta-stream**
(`spt digest <id> --follow` — only changed turns are pushed). Local addressing only;
the qualified `[subnet:]id@node` cross-node form is **M4**.

### `[update]` — adapter self-update
```toml
[update]
avenue = "delegated"               # delegated | file_pull
command = "claude plugin update spt"          # delegated
# file_pull: repo + path_regex
version_check = true               # verify spt-core satisfies min_spt_core_version before/after
uninstall = "claude plugin uninstall spt"     # optional; mirror of install — runs on `adapter remove` once quiesced
```

`spt adapter add` reuses this section as the **install** mechanism (install is the first update; `--github <user/repo>` fetches the manifest first, then installs via the declared `avenue`). `uninstall` (optional) is its inverse, fired by `spt adapter remove` (soft-deregister; runs when no instance is live under the adapter, or immediately with `--force`). Absent `uninstall` ⇒ spt-core's default cleanup (drop its copy for `file_pull`; for `delegated`, just deregister and leave the harness's own files).

---

## Shell adapters (`kind = "shell"`)

A second kind of adapter. Provides a `Shell` endpoint (a driven surface) rather than hosting agents. Same file location (`adapters/`), different manifest body.

```toml
[adapter]
name = "GameRobot"
kind = "shell"                     # vs "harness"
version = "1.0.0"
min_spt_core_version = "1.0.0"

[shell]
spawn = "gamerobot-shell --link {link_token}"   # broker-launched; opaque command template
ephemeral = false                  # manifest property, NOT an agent choice; ephemeral ⇒ no offline perch + no history retention
broadcast = "subnet"               # subnet | same-node | none
command_receipt = "http"           # http | stdin | relay   (how it receives agent commands)
pre_close = "park-and-save"        # optional instruction sent to the binary on link-break
close_timeout_ms = 3000            # graceful termination window before force-close
persistent = true                  # auto-online whenever the owner endpoint is online
wake_command = "gamerobot-waker --link {link_token}"  # long-running wake-watcher run WHILE offline; exit(wake-opcode) ⇒ revive
can_shutdown = false               # if true, the shell may fire `api owner-shutdown` to suspend its owner directly
require_approval = "none"          # none | remembered | always — per-spawn user-approval gate (floor; node/endpoint may tighten)
max_instances_per_owner = 4        # optional cap on existing instances per owner (online+offline both count); omit ⇒ unlimited
over_cap = "reject"                # reject | approve — at the cap: refuse, or require per-spawn approval beyond it (doesn't raise the cap)

[shell.capabilities]               # the command vocabulary (agent→shell) — the toolset
move      = { args = ["direction", "distance"] }
gesture   = { args = ["name"] }
alert     = { args = ["symbol"] }
screenshot = {}

[shell.sensory]                    # the sensory vocabulary (shell→agent, REST-only)
types = ["image", "sound", "event"]   # "event" = arbitrary descriptive payloads
```

The capability + sensory vocabularies live **here**, not on the perch — spt-core resolves them by `adapter_name`. Command-delivery (`command_receipt`) reuses the agent inject-input modes. The shell binary binds via `api bind` (type=Shell, owner from the link) and pushes sensory via `api emit --type <t>`.

**Sleep/wake:** when offline (+ `wake_command` set), spt-core runs the wake-watcher (mutually exclusive with the shell binary); `exit(wake-opcode)` → wake resolution; crash-exit → respawn with exponential backoff + give-up (until the next relink/launch). **The wake opcode is exit code 86.** The wake-watcher template shares the spawn template's substitution catalog (`{id}`, `{adapter_name}`, `{link_token}`), but its `{link_token}` is minted *unparked* — an offline link has no live credential (the close retired it); a waker wakes by exit code, never by driving the link. `persistent` auto-onlines the shell with its owner. `can_shutdown` authorizes `api owner-shutdown`. Cross-node *fresh-spawn* to wake an owner requires the owner's `shell_wake_spawn_anywhere` settings flag (pre-consent).

**Instantiation governance:** `require_approval` gates `shell spawn` (reusing the consent grant store — `remembered` persists a grant via allow-always, `always` prompts every spawn); `max_instances_per_owner` + `over_cap` cap how many instances one owner may hold (online + offline both count toward the cap). An instance's **alias** is **not** a manifest field — it is per-instance runtime state on the shell perch's `info.json` (set at `shell spawn --alias` / `shell rename`), since the manifest describes the *adapter*, not individual instances.

## Worked example: `claude-spt` (today's behavior, expressed)

Maps the sister project's actual surface onto the schema.

| Concern | Today (modern SPT) | Manifest expression |
|---|---|---|
| 7 CC hooks | hooks.json → `owl.exe <subcmd>` | `[hooks.*]` table; `Stop.can_inject=false` drives the echo-gate fallback |
| Psyche init | `claude -p --agents … --model sonnet --fallback-model opus …` | `[session.psyche_init].command` (opaque) |
| Psyche resume | `claude -p --resume <uuid> --model sonnet …` | `[session.psyche_resume]` + `{session_id}` |
| Echo-commune | `claude -p --model haiku --effort low …`, `OWL_ECHO_COMMUNE=1` guard | `[session.echo_commune]` + `recursion_guard_env` |
| `ccs` fallback | tier-2 binary swap | **node-wide cross-adapter fallback** (not manifest; `ccs` is its own adapter) |
| Transcript read | `.claude/projects/<enc-cwd>/<uuid>.jsonl` | `[history]` (fetcher or locate_normalize) |
| Commune/signoff | `.claude/<id>-commune.md` watched | `[session].commune_dir/signoff_dir`; filenames fixed |
| Env | `$OWL`/`$LIVE`/`OWL_SESSION_ID` via `CLAUDE_ENV_FILE` | `[env]`; PATH removes most; channel only harness-hosted |
| Identity | `OWL_SESSION_ID` + `claude.exe` ancestor walk | `[identity]` |
| Update | (today: cplugs) | `[update] avenue="delegated" command="claude plugin update"` |
| period / echo-window / route-guard / 24h refresh | hardcoded/flags | spt-core **global knobs** (per-endpoint override), not manifest |

## Open sub-items
- Exact substitution-key catalog (which keys spt-core guarantees to fill per role).
- TOML vs YAML final choice; schema-validation + helpful errors for adapter authors.
- The fixed event-block vocabulary's final tag names (`<SPT_EVENT>` etc.).
- Capability-declaration shape beyond `hostable_types` (per-type operation advertisement).


mdbook --version 2>&1; gh run list -R BigscreenVR/traceable-reqs --limit 2 2>&1; gh run list -R SaberMage/spt-core --branch dev-freeform --limit 1

mdbook check + release/CI status


mdbook: The term 'mdbook' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
completed	success	Support shell and PowerShell scanning	Release	v0.1.2	push	27033655199	2m12s	2026-06-05T18:46:11Z
completed	success	Support shell and PowerShell scanning	CI	main	push	27033654323	30s	2026-06-05T18:46:10Z
in_progress		ci: build the pinned traceable-reqs outside the checkout (RUNNER_TEMP)	ci	dev-freeform	push	27034233324	56s	2026-06-05T18:58:13Z


cargo install mdbook --locked 2>&1 | Select-Object -Last 3

Install mdbook (background)


Command running in background with ID: bx5dnnkv9. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bx5dnnkv9.output. You will be notified when it completes. To check interim output, use Read on that file path.


Write

# spt-core developer docs — mdBook config (M6-D4, DOCS-STRATEGY §v0.1).
# Published to GitHub Pages on SaberMage/spt-releases at the permanent
# canonical URL (ADR-0014): https://sabermage.github.io/spt-releases
[book]
title = "SPT developer docs"
description = "spt-core: a harness-independent core for an agent ecosystem — messaging, live-agent lifecycle, terminal hosting, P2P networking, and the runtime-manifest harness contract."
authors = ["SaberMage"]
language = "en"
src = "src"

[build]
create-missing = false

[output.html]
default-theme = "ayu"
preferred-dark-theme = "ayu"
additional-css = ["theme/spt.css"]
git-repository-url = "https://github.com/SaberMage/spt-releases"
site-url = "/spt-releases/"

[output.html.fold]
enable = true
level = 1


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

# spt-core developer docs — mdBook config (M6-D4, DOCS-STRATEGY §v0.1).
# Published to GitHub Pages on SaberMage/spt-releases at the permanent
# canonical URL (ADR-0014): https://sabermage.github.io/spt-releases
[book]
title = "SPT developer docs"
description = "spt-core: a harness-independent core for an agent ecosystem — messaging, live-agent lifecycle, terminal hosting, P2P networking, and the runtime-manifest harness contract."
authors = ["SaberMage"]
language = "en"
src = "src"

[build]
create-missing = false

[output.html]
default-theme = "ayu"
preferred-dark-theme = "ayu"
additional-css = ["theme/spt.css"]
git-repository-url = "https://github.com/SaberMage/spt-releases"
site-url = "/spt-releases/"

[output.html.fold]
enable = true
level = 1


# Summary

[SPT developer docs](index.md)

# Getting started

- [Quickstart: two agents exchange a message](quickstart/messaging.md)
- [Quickstart: build an adapter](quickstart/adapter.md)
- [Mental model](concepts/overview.md)

# Capabilities

- [Messaging](messaging/overview.md)
- [Live-agent lifecycle](lifecycle/overview.md)
- [Terminal hosting](terminal/overview.md)
- [Networking & pairing](networking/overview.md)
- [Harness contract](harness-contract/overview.md)
  - [Manifest reference](harness-contract/manifest.md)
  - [The `spt api` surface](harness-contract/api.md)
  - [Install-on-demand bootstrap](harness-contract/install-on-demand.md)
- [Instances](instances/overview.md)
- [Shells](shells/overview.md)
  - [Getting started: a notification shell](shells/getting-started.md)
- [Self-update](self-update/overview.md)

# Reference

- [CLI reference](cli/reference.md)
- [Manifest JSON Schema](reference/schema.md)
- [Install scripts](reference/install.md)