2387	fn origin_asset_url(repo: &str, tag: Option<&str>, asset: &str) -> String {
2388	    format!("{}/{asset}", origin_download_base(repo, tag))
2389	}
2390
2391	fn http_get_text(client: &reqwest::blocking::Client, url: &str) -> Result<String, String> {
2392	    let resp = client.get(url).send().map_err(|e| e.to_string())?;
2393	    if !resp.status().is_success() {
2394	        return Err(format!("HTTP {}", resp.status()));
2395	    }
2396	    resp.text().map_err(|e| e.to_string())
2397	}
2398
2399	fn http_get_bytes(client: &reqwest::blocking::Client, url: &str) -> Result<Vec<u8>, String> {
2400	    let resp = client.get(url).send().map_err(|e| e.to_string())?;
2401	    if !resp.status().is_success() {
2402	        return Err(format!("HTTP {}", resp.status()));
2403	    }
2404	    resp.bytes().map(|b| b.to_vec()).map_err(|e| e.to_string())
2405	}
2406
2407	/// Extract a `.spt` adapter archive into `dest` via the system `tar`, bounded
2408	/// (REQ-HAZARD-SUBPROCESS-TIMEOUT). `.spt` is a tar; `tar -xf` auto-detects an
2409	/// optional gzip, so a dev may ship either form. The archive's root holds the
2410	/// manifest + `strings/` + binaries, so they land directly under `dest`.
2411	// [impl->REQ-INSTALL-9]
2412	fn extract_release_archive(archive: &std::path::Path, dest: &std::path::Path) -> Result<(), String> {
2413	    let keys = std::collections::BTreeMap::from([
2414	        ("archive".to_string(), archive.to_string_lossy().into_owned()),
2415	        ("dest".to_string(), dest.to_string_lossy().into_owned()),
2416	    ]);

fn origin_asset_url(repo: &str, tag: Option<&str>, asset: &str) -> String {
    format!("{}/{asset}", origin_download_base(repo, tag))
}

fn http_get_text(client: &reqwest::blocking::Client, url: &str) -> Result<String, String> {
    let resp = client.get(url).send().map_err(|e| e.to_string())?;
    if !resp.status().is_success() {
        return Err(format!("HTTP {}", resp.status()));
    }
    resp.text().map_err(|e| e.to_string())
}

fn http_get_bytes(client: &reqwest::blocking::Client, url: &str) -> Result<Vec<u8>, String> {
    let resp = client.get(url).send().map_err(|e| e.to_string())?;
    if !resp.status().is_success() {
        return Err(format!("HTTP {}", resp.status()));
    }
    resp.bytes().map(|b| b.to_vec()).map_err(|e| e.to_string())
}

/// Extract a `.spt` adapter archive into `dest` via the system `tar`, bounded
/// (REQ-HAZARD-SUBPROCESS-TIMEOUT). `.spt` is a tar; `tar -xf` auto-detects an
/// optional gzip, so a dev may ship either form. The archive's root holds the
/// manifest + `strings/` + binaries, so they land directly under `dest`.
// [impl->REQ-INSTALL-9]
fn extract_release_archive(archive: &std::path::Path, dest: &std::path::Path) -> Result<(), String> {
    let keys = std::collections::BTreeMap::from([
        ("archive".to_string(), archive.to_string_lossy().into_owned()),
        ("dest".to_string(), dest.to_string_lossy().into_owned()),
    ]);


5715	fn gh_latest_release_version(repo: &str) -> Result<String, String> {
5716	    let url = format!("https://api.github.com/repos/{repo}/releases/latest");
5717	    let client = reqwest::blocking::Client::builder()
5718	        .user_agent(concat!("spt/", env!("CARGO_PKG_VERSION")))
5719	        .build()
5720	        .map_err(|e| e.to_string())?;
5721	    let body = http_get_text(&client, &url)?;
5722	    let v: serde_json::Value = serde_json::from_str(&body).map_err(|e| e.to_string())?;
5723	    let tag = v
5724	        .get("tag_name")
5725	        .and_then(|t| t.as_str())
5726	        .ok_or("release JSON had no tag_name")?;
5727	    Ok(tag.strip_prefix('v').unwrap_or(tag).to_string())
5728	}
5729
5730	/// Compare two dotted-numeric versions (`a` newer than `b`?). Hand-rolled rather
5731	/// than pulling a semver dependency: splits on `.`, compares each numeric
5732	/// component, treats a missing component as 0, and a non-numeric component as 0
5733	/// (best-effort — a release tag spt-core can't order is treated as not-newer, so
5734	/// an unparseable tag never triggers an unrequested update). (REQ-UPD-9)
5735	// [impl->REQ-UPD-9]
5736	fn version_is_newer(a: &str, b: &str) -> bool {
5737	    let part = |s: &str| -> Vec<u64> {
5738	        s.split('.')
5739	            .map(|c| c.trim().parse::<u64>().unwrap_or(0))
5740	            .collect()
5741	    };
5742	    let (av, bv) = (part(a), part(b));
5743	    let n = av.len().max(bv.len());
5744	    for i in 0..n {

fn gh_latest_release_version(repo: &str) -> Result<String, String> {
    let url = format!("https://api.github.com/repos/{repo}/releases/latest");
    let client = reqwest::blocking::Client::builder()
        .user_agent(concat!("spt/", env!("CARGO_PKG_VERSION")))
        .build()
        .map_err(|e| e.to_string())?;
    let body = http_get_text(&client, &url)?;
    let v: serde_json::Value = serde_json::from_str(&body).map_err(|e| e.to_string())?;
    let tag = v
        .get("tag_name")
        .and_then(|t| t.as_str())
        .ok_or("release JSON had no tag_name")?;
    Ok(tag.strip_prefix('v').unwrap_or(tag).to_string())
}

/// Compare two dotted-numeric versions (`a` newer than `b`?). Hand-rolled rather
/// than pulling a semver dependency: splits on `.`, compares each numeric
/// component, treats a missing component as 0, and a non-numeric component as 0
/// (best-effort — a release tag spt-core can't order is treated as not-newer, so
/// an unparseable tag never triggers an unrequested update). (REQ-UPD-9)
// [impl->REQ-UPD-9]
fn version_is_newer(a: &str, b: &str) -> bool {
    let part = |s: &str| -> Vec<u64> {
        s.split('.')
            .map(|c| c.trim().parse::<u64>().unwrap_or(0))
            .collect()
    };
    let (av, bv) = (part(a), part(b));
    let n = av.len().max(bv.len());
    for i in 0..n {


Confirmed the current transport: the spt binary fetches adapters by **direct HTTPS via reqwest** — `http_get_bytes` against `github.com/…/releases/download/…`, and `api.github.com/…/releases/latest` for the version (cli.rs:2391-2405, 5715). **No `gh` shelling** in the binary's fetch path today (gh is an ecosystem assumption for publishing/sync, not used here). So for a **private** repo the current path just 403/404s — no auth.

**On your gh-cli idea: yes, and I think it's the best private-repo path, not just a viable one.** Reasons:
- `gh` is already authenticated — and it honors **both** interactive OAuth **and** `GH_TOKEN` — so one transport covers dev *and* CI/headless private fetches.
- spt **never touches a token** → smaller security surface than the alternative (a `GITHUB_TOKEN` env/config that spt reads and attaches as an `Authorization` header to reqwest). No PAT minting, no token in spt's config.
- The audience pulling **private** adapters is adapter authors/operators, who already have `gh`.
- `gh release download <tag> --repo U/r --pattern adapter.spt` + `gh api repos/U/r/releases/latest` map 1:1 onto the two HTTPS calls we already make; the verify→extract→register path downstream is **identical** (the `.sig` fetches the same way).

The one tradeoff: the private path then **requires `gh` installed** — but public adapters keep working over plain HTTPS with no `gh`. (The token-HTTPS alternative avoids the gh dep but puts token custody inside spt — worse for the interactive case you're describing.)

**Q (private-repo transport).** How do we model it?

- **(A — recommended)** A **transport** toggle on the existing `gh_release` avenue: `transport = "https" | "gh" | "auto"`, default **`auto`** (use `gh` if installed+authed, else HTTPS). Same avenue, same `repo`/`asset`/`signing_key` fields — only the byte-fetch differs. `spt adapter add --release U/r` mirrors it: auto-detect, with `--gh`/`--https` to force. Private "just works" when `gh` is present; public needs nothing new.
- **(B)** A distinct `avenue = "gh_cli"` parallel to `gh_release` — explicit in the manifest, but duplicates the repo/asset/signing fields for what is really the same "fetch a release asset, different transport."

**Recommendation: (A), default `auto`.** It's additive (not a format/protocol change), so I don't think it needs its own ADR — it folds into the v0.13.2 `[update]` work.

Confirm **(A) + `auto` default**? Then we've covered #1, #2/#3, and the private-repo path — leaving just **Q4 (`update-message`)**, and I'll write up the two ADRs + the CONTEXT terms on the v0.13.2 branch.


confirm recs


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


All recs confirmed and locked. Tally so far:
- **#1** — (A) multi-platform `.spt`, triple-subdirs, name stays `adapter.spt`, one whole-archive sig, `--asset` optional.
- **private transport** — (A) `transport` toggle on `gh_release`, default `auto` (gh if installed+authed, else HTTPS); `--gh`/`--https` override on `add`.
- **#2+#3** — daemon-coordinated live-cycle: stop resident binary → CRC-gated swap → refresh in-memory manifest → restart; the adapter analog of ADR-0004 brain self-update.

---

**Q4 — `update-message` (the easy one).** A few sub-decisions; my recommended answers inline:

1. **Where:** `[update].message` (lives in the `[update]` table, avenue-agnostic — shows for gh_release / delegated / file_pull alike).
2. **Which copy:** the **newly-installed** manifest's message (it describes the new version's changes — e.g. "the new plugin needs `/reload-plugins`"), read after extract.
3. **When — the one real question:** you said "anytime `spt adapter update` is run." I'd narrow it to **only when an update is actually applied** (version changed), **not on a no-op "already up to date."** Rationale: the message is a post-update action ("reload plugins in ongoing sessions") — on a no-op nothing changed, so it's noise that trains people to ignore it. *Recommend: applied-update only.* (Tell me if you really want it on every invocation.)
4. **Rendering:** route it through the **markdown prose renderer** (the v0.13.0 helpfmt path) — your example has `` `/reload-plugins` `` in backticks, so it renders as styled inline code, consistent with the rest of spt's prose output.
5. **Format:** plain multi-line string, **no `{key}` substitution** (YAGNI — it's a human notice, not a template).

Confirm those (especially #3: **applied-update only**), and that closes the grill. Then I'll cut the **v0.13.2 branch** off `origin/main` and write up: **ADR (multi-platform `.spt` packaging)**, **ADR (live daemon-coordinated adapter update)**, the **CONTEXT.md** terms (`.spt` updated, *translation binary* / *resident adapter binary*, `[update]` transport), and fold the **translate-proof checklist/patterns** doc gap into the same branch's docs.


Q4: agree with recs


Q4 confirmed — **grill complete, full tree walked.** Now I cut the v0.13.2 branch off `origin/main` and write up the artifacts. First, set up the worktree + check ADR numbering (your unmerged ADR-0023 on c84daff matters for collision-avoidance).


Bash

git fetch origin main 2>&1 | tail -1; echo "=== origin/main HEAD ==="; git log --oneline -1 origin/main
echo "=== ADR numbers on origin/main ==="; git ls-tree -r --name-only origin/main -- docs/adr 2>&1 | grep -oE '00[0-9][0-9]' | sort -u | tail -8
echo "=== ADR numbers on c84daff (local, operator's) ==="; git ls-tree -r --name-only c84daff -- docs/adr 2>&1 | grep -oE '00[0-9][0-9]' | sort -u | tail -5
echo "=== create v0.13.2 worktree ==="; git worktree add -b v0.13.2-adapter-packaging "C:/Users/decid/Documents/projects/spt-core-v0132" origin/main 2>&1 | tail -2

Recon ADR numbers and create v0.13.2 worktree


 * branch            main       -> FETCH_HEAD
=== origin/main HEAD ===
4d5a0cb release: v0.13.1 — bump 0.13.0->0.13.1 + changelog + regen CLI reference
=== ADR numbers on origin/main ===
0015
0016
0017
0018
0019
0020
0021
0022
=== ADR numbers on c84daff (local, operator's) ===
0018
0019
0020
0021
0023
=== create v0.13.2 worktree ===
branch 'v0.13.2-adapter-packaging' set up to track 'origin/main'.
HEAD is now at 4d5a0cb release: v0.13.1 — bump 0.13.0->0.13.1 + changelog + regen CLI reference


Worktree ready on `v0.13.2-adapter-packaging` (off `origin/main` 4d5a0cb). ADR numbering: origin/main tops out at **0022**; your unmerged **0023** (always-on endpoint) sits on c84daff — so v0.13.2 takes **0024 + 0025** (flagging the 0023 dependency: if your always-on-endpoint ADR renumbers, these shift). Writing the artifacts.


Write

# Multi-platform adapter `.spt` packaging

<!-- [doc->REQ-ADAPTER-MULTIPLATFORM-SPT] -->

## Status

proposed (2026-06-22, v0.13.2)

Depends on the target-triple vocabulary established by ADR-0016 (platform-targeted update sets). Sibling to ADR-0016: that ADR governs spt-core's *own* binary distribution and explicitly deferred adapter inclusion ("adapter updates remain on the adapter ripple-update seam… maintainer tooling may compose adapter updates later"). This ADR is that deferred adapter work.

## Context

An adapter that updates via the `gh_release` avenue is fetched as a single release asset, `adapter.spt` — a tar whose root holds `manifest.toml` + `strings/` + the role binaries, extracted flat into the adapter's `install_dir`, where bare program tokens resolve `<install_dir>/<program>` before `PATH` (REQ-INSTALL-11). That format is **single-platform**: one archive carries one OS's binaries. A cross-platform adapter (e.g. a harness integration shipping a Windows `.exe` and a Linux ELF for its translation/digest/psyche binaries) cannot ship one artifact.

spt-core's own binary already solves cross-platform distribution with **platform-targeted update sets** (ADR-0016): per-triple *separate* artifacts, one signed set-metadata blob with per-platform digests, and each recipient fetches only its own platform's bytes. That model is bandwidth-optimal at fleet scale but heavier for an adapter author: multiple artifacts plus signed set metadata, for what is often a few kilobytes of binary.

## Decision

Add a **fat multi-platform `.spt`** as the simple default, keeping the per-triple "split" path available for adapters that want it.

- **Layout.** The archive root holds the shared `manifest.toml` + `strings/`. Platform binaries live under **per-target-triple subdirectories** keyed by Rust target triple (ADR-0016's protocol identifiers, e.g. `x86_64-pc-windows-msvc/`, `x86_64-unknown-linux-gnu/`). A legacy single-platform archive keeps its binaries at the root.

  ```
  manifest.toml                         (shared)
  strings/                              (shared)
  x86_64-pc-windows-msvc/   <binaries>
  x86_64-unknown-linux-gnu/ <binaries>
  ```

- **Selection at install/update.** spt-core extracts the shared root **plus only `current_platform()`'s** triple subdir, flattened into `install_dir`. So `<install_dir>/<program>` resolution (REQ-INSTALL-11) is **unchanged** — binaries land at the same flat path they do today; nothing downstream moves. An archive that declares triple subdirs but is missing the recipient's triple yields a typed `NoArtifactForPlatform`, never a silent no-op (mirrors ADR-0016).

- **Naming.** The canonical release asset stays **`adapter.spt`** (no new extension). It may be plain tar or gzipped — `tar -xf` auto-detects — so an author ships either form as one asset. `--asset` becomes **optional**, defaulting to `adapter.spt`, so `spt adapter add --release User/repo` and `spt adapter update` work flag-free.

- **Signing.** A fat `adapter.spt` is **one artifact → one signature**: the adapter's existing `signing_key` Ed25519 over the whole archive (`adapter.spt.sig`), the REQ-UPD-9 single-artifact verify, just over more bytes.

- **The split path.** A large adapter that does not want every node downloading all platforms' bytes publishes **per-platform single-triple archives** (selected via `--asset`, or composed under ADR-0016's signed update-set machinery). This reuses existing mechanisms — there is no second bespoke multi-platform system.

- **Compatibility.** Forward compat is the existing `min_spt_core_version` gate: a multi-platform adapter sets `min_spt_core_version ≥ 0.13.2`, so an older spt-core (which would extract the triple subdirs flat and fail to resolve `<install_dir>/<program>`) refuses at the gate — readable *before* extract — instead of mis-installing. Back compat is structural and free: a flat archive has no triple subdirs, so new spt-core extracts it exactly as today. (Elaborate legacy/auto-detection beyond "is there a triple subdir?" is unwarranted — there is no installed userbase to migrate.)

## Considered Options

- **Per-triple update-set for adapters (ADR-0016 model as the default)** — consistent with spt-core and bandwidth-optimal, but heavier authoring (multiple artifacts + signed set metadata) and worse small-adapter UX. *Kept as the opt-in "split" path, not the default.*
- **Manifest-declared platform map with suffixed binary names at the root** — invents an adapter-only platform-naming scheme divergent from ADR-0016's triples and mangles binary names. Rejected.
- **Fat multi-platform `.spt` with triple subdirs (chosen)** — one artifact, one signature, one flag-free fetch; flat install layout unchanged; reuses ADR-0016's triple vocabulary; split stays available. Trade-off accepted: a fat archive ships every platform's bytes to every recipient — negligible for the small adapters this targets; large adapters split.


File created successfully at: C:/Users/decid/Documents/projects/spt-core-v0132/docs/adr/0024-multi-platform-adapter-spt-packaging.md (file state is current in your context — no need to Read it back)

# Multi-platform adapter `.spt` packaging

<!-- [doc->REQ-ADAPTER-MULTIPLATFORM-SPT] -->

## Status

proposed (2026-06-22, v0.13.2)

Depends on the target-triple vocabulary established by ADR-0016 (platform-targeted update sets). Sibling to ADR-0016: that ADR governs spt-core's *own* binary distribution and explicitly deferred adapter inclusion ("adapter updates remain on the adapter ripple-update seam… maintainer tooling may compose adapter updates later"). This ADR is that deferred adapter work.

## Context

An adapter that updates via the `gh_release` avenue is fetched as a single release asset, `adapter.spt` — a tar whose root holds `manifest.toml` + `strings/` + the role binaries, extracted flat into the adapter's `install_dir`, where bare program tokens resolve `<install_dir>/<program>` before `PATH` (REQ-INSTALL-11). That format is **single-platform**: one archive carries one OS's binaries. A cross-platform adapter (e.g. a harness integration shipping a Windows `.exe` and a Linux ELF for its translation/digest/psyche binaries) cannot ship one artifact.

spt-core's own binary already solves cross-platform distribution with **platform-targeted update sets** (ADR-0016): per-triple *separate* artifacts, one signed set-metadata blob with per-platform digests, and each recipient fetches only its own platform's bytes. That model is bandwidth-optimal at fleet scale but heavier for an adapter author: multiple artifacts plus signed set metadata, for what is often a few kilobytes of binary.

## Decision

Add a **fat multi-platform `.spt`** as the simple default, keeping the per-triple "split" path available for adapters that want it.

- **Layout.** The archive root holds the shared `manifest.toml` + `strings/`. Platform binaries live under **per-target-triple subdirectories** keyed by Rust target triple (ADR-0016's protocol identifiers, e.g. `x86_64-pc-windows-msvc/`, `x86_64-unknown-linux-gnu/`). A legacy single-platform archive keeps its binaries at the root.

  ```
  manifest.toml                         (shared)
  strings/                              (shared)
  x86_64-pc-windows-msvc/   <binaries>
  x86_64-unknown-linux-gnu/ <binaries>
  ```

- **Selection at install/update.** spt-core extracts the shared root **plus only `current_platform()`'s** triple subdir, flattened into `install_dir`. So `<install_dir>/<program>` resolution (REQ-INSTALL-11) is **unchanged** — binaries land at the same flat path they do today; nothing downstream moves. An archive that declares triple subdirs but is missing the recipient's triple yields a typed `NoArtifactForPlatform`, never a silent no-op (mirrors ADR-0016).

- **Naming.** The canonical release asset stays **`adapter.spt`** (no new extension). It may be plain tar or gzipped — `tar -xf` auto-detects — so an author ships either form as one asset. `--asset` becomes **optional**, defaulting to `adapter.spt`, so `spt adapter add --release User/repo` and `spt adapter update` work flag-free.

- **Signing.** A fat `adapter.spt` is **one artifact → one signature**: the adapter's existing `signing_key` Ed25519 over the whole archive (`adapter.spt.sig`), the REQ-UPD-9 single-artifact verify, just over more bytes.

- **The split path.** A large adapter that does not want every node downloading all platforms' bytes publishes **per-platform single-triple archives** (selected via `--asset`, or composed under ADR-0016's signed update-set machinery). This reuses existing mechanisms — there is no second bespoke multi-platform system.

- **Compatibility.** Forward compat is the existing `min_spt_core_version` gate: a multi-platform adapter sets `min_spt_core_version ≥ 0.13.2`, so an older spt-core (which would extract the triple subdirs flat and fail to resolve `<install_dir>/<program>`) refuses at the gate — readable *before* extract — instead of mis-installing. Back compat is structural and free: a flat archive has no triple subdirs, so new spt-core extracts it exactly as today. (Elaborate legacy/auto-detection beyond "is there a triple subdir?" is unwarranted — there is no installed userbase to migrate.)

## Considered Options

- **Per-triple update-set for adapters (ADR-0016 model as the default)** — consistent with spt-core and bandwidth-optimal, but heavier authoring (multiple artifacts + signed set metadata) and worse small-adapter UX. *Kept as the opt-in "split" path, not the default.*
- **Manifest-declared platform map with suffixed binary names at the root** — invents an adapter-only platform-naming scheme divergent from ADR-0016's triples and mangles binary names. Rejected.
- **Fat multi-platform `.spt` with triple subdirs (chosen)** — one artifact, one signature, one flag-free fetch; flat install layout unchanged; reuses ADR-0016's triple vocabulary; split stays available. Trade-off accepted: a fat archive ships every platform's bytes to every recipient — negligible for the small adapters this targets; large adapters split.


# Live, daemon-coordinated adapter update

<!-- [doc->REQ-ADAPTER-LIVE-UPDATE] -->

## Status

proposed (2026-06-22, v0.13.2)

Extends spt's seamless-self-update objective (ADR-0004, broker/brain split) from spt-core's own brain to **adapters**.

## Context

`spt adapter update` runs **CLI-side**: it fetches, optionally verifies, and re-registers the adapter on disk (REQ-UPD-7/9 deliberately keep HTTP off the daemon). It is **daemon-blind** — it never coordinates with the running daemon. Two facts make that unsafe while an endpoint is live:

1. **A running endpoint caches the manifest in memory.** A `BrainLifecycle` clones the adapter `Manifest` at endpoint bringup (`lifecycle.rs`, the `manifest` field + the `ManifestRuntime` clone) and never re-reads it. After a disk update the in-memory manifest is **stale** — the running endpoint drives new binaries against an old manifest, or an old binary path against a new layout.

2. **The daemon runs resident adapter binaries that hold file locks.** spt-core keeps a **resident adapter binary** alive for an endpoint's lifetime — today the `[message-idle-translation-binary]` (spawned when the spt-hosted endpoint comes up, killed when it goes down); historically, whatever adapter file the daemon held open. On Windows a running `.exe` cannot be overwritten, so the CLI update's `remove_dir_all` + extract fails with *"Access is denied (os error 5)"*. This is the locked-binary failure observed in the field.

A blunt fix — refuse to update while any endpoint on the adapter is live (the stance ADR-0016 took for broker-touching core updates) — contradicts spt's core objective: on-the-fly updates. The brain already self-updates without terminating endpoints; adapters should too.

## Decision

When an adapter has **running resident binaries**, its update becomes **daemon-coordinated**. The CLI keeps fetch + verify (no new daemon HTTP surface); it then hands the **apply** to the daemon over IPC. The daemon apply, per affected endpoint:

1. **Stop** the adapter's resident binaries → releases the OS file lock (fixes the Windows "Access denied" overwrite failure).
2. **CRC-gated swap.** Replace on disk **only files whose CRC differs** from the staged archive; identical files (and their still-running binaries, when unchanged) are left untouched. Avoids needless rewrites and needless restarts.
3. **Refresh the in-memory manifest.** Re-clone the new on-disk manifest into the running `BrainLifecycle`(s), so binaries and manifest are back on the same page.
4. **Restart** the resident binaries from the new files.

An endpoint **not** running goes the simple path: the CLI swaps files directly — no lock, no cached manifest, no daemon round-trip.

**Resident vs ephemeral.** Only the **resident** class — binaries spt-core keeps alive for an endpoint's lifetime (today: the translation binary; tomorrow: any future long-lived adapter-owned process) — is stopped and restarted. **Ephemeral** adapter binaries self-heal and are excluded: the psyche loop is daemon-hosted, not a separate process (ADR-0004); the `[digest]` extractor, `[session.*]` runners, and hooks are spawned on demand, so their next invocation simply execs the updated file. The daemon keeps a **per-endpoint registry of resident adapter children** so an apply knows exactly which live processes to cycle.

**CRC** is plain content hashing of each archived file vs its installed counterpart; the goal is "replace only what changed," not cryptographic integrity (that is the archive signature, ADR-0024 / REQ-UPD-9).

## Considered Options

- **Refuse-while-running (ADR-0016 broker parity)** — update refuses if any endpoint on the adapter is live, lists them, and tells the operator to stop them first. Simple and consistent with the core broker-replace stance, but it is exactly the UX spt's on-the-fly objective rejects, and it does not deliver the live-update the brain already has. Rejected for adapters.
- **CLI-only swap with a retry/backoff on the lock** — does nothing for the stale in-memory manifest, and on Windows a running binary's lock never clears without stopping the process. Rejected.
- **Daemon-coordinated stop → CRC-swap → manifest-refresh → restart (chosen)** — the adapter analog of brain self-update; the only option that fixes both the locked-binary failure and the stale-manifest, at the cost of a daemon IPC apply path plus the per-endpoint resident-children registry.


File created successfully at: C:/Users/decid/Documents/projects/spt-core-v0132/docs/adr/0025-live-daemon-coordinated-adapter-update.md (file state is current in your context — no need to Read it back)

# Live, daemon-coordinated adapter update

<!-- [doc->REQ-ADAPTER-LIVE-UPDATE] -->

## Status

proposed (2026-06-22, v0.13.2)

Extends spt's seamless-self-update objective (ADR-0004, broker/brain split) from spt-core's own brain to **adapters**.

## Context

`spt adapter update` runs **CLI-side**: it fetches, optionally verifies, and re-registers the adapter on disk (REQ-UPD-7/9 deliberately keep HTTP off the daemon). It is **daemon-blind** — it never coordinates with the running daemon. Two facts make that unsafe while an endpoint is live:

1. **A running endpoint caches the manifest in memory.** A `BrainLifecycle` clones the adapter `Manifest` at endpoint bringup (`lifecycle.rs`, the `manifest` field + the `ManifestRuntime` clone) and never re-reads it. After a disk update the in-memory manifest is **stale** — the running endpoint drives new binaries against an old manifest, or an old binary path against a new layout.

2. **The daemon runs resident adapter binaries that hold file locks.** spt-core keeps a **resident adapter binary** alive for an endpoint's lifetime — today the `[message-idle-translation-binary]` (spawned when the spt-hosted endpoint comes up, killed when it goes down); historically, whatever adapter file the daemon held open. On Windows a running `.exe` cannot be overwritten, so the CLI update's `remove_dir_all` + extract fails with *"Access is denied (os error 5)"*. This is the locked-binary failure observed in the field.

A blunt fix — refuse to update while any endpoint on the adapter is live (the stance ADR-0016 took for broker-touching core updates) — contradicts spt's core objective: on-the-fly updates. The brain already self-updates without terminating endpoints; adapters should too.

## Decision

When an adapter has **running resident binaries**, its update becomes **daemon-coordinated**. The CLI keeps fetch + verify (no new daemon HTTP surface); it then hands the **apply** to the daemon over IPC. The daemon apply, per affected endpoint:

1. **Stop** the adapter's resident binaries → releases the OS file lock (fixes the Windows "Access denied" overwrite failure).
2. **CRC-gated swap.** Replace on disk **only files whose CRC differs** from the staged archive; identical files (and their still-running binaries, when unchanged) are left untouched. Avoids needless rewrites and needless restarts.
3. **Refresh the in-memory manifest.** Re-clone the new on-disk manifest into the running `BrainLifecycle`(s), so binaries and manifest are back on the same page.
4. **Restart** the resident binaries from the new files.

An endpoint **not** running goes the simple path: the CLI swaps files directly — no lock, no cached manifest, no daemon round-trip.

**Resident vs ephemeral.** Only the **resident** class — binaries spt-core keeps alive for an endpoint's lifetime (today: the translation binary; tomorrow: any future long-lived adapter-owned process) — is stopped and restarted. **Ephemeral** adapter binaries self-heal and are excluded: the psyche loop is daemon-hosted, not a separate process (ADR-0004); the `[digest]` extractor, `[session.*]` runners, and hooks are spawned on demand, so their next invocation simply execs the updated file. The daemon keeps a **per-endpoint registry of resident adapter children** so an apply knows exactly which live processes to cycle.

**CRC** is plain content hashing of each archived file vs its installed counterpart; the goal is "replace only what changed," not cryptographic integrity (that is the archive signature, ADR-0024 / REQ-UPD-9).

## Considered Options

- **Refuse-while-running (ADR-0016 broker parity)** — update refuses if any endpoint on the adapter is live, lists them, and tells the operator to stop them first. Simple and consistent with the core broker-replace stance, but it is exactly the UX spt's on-the-fly objective rejects, and it does not deliver the live-update the brain already has. Rejected for adapters.
- **CLI-only swap with a retry/backoff on the lock** — does nothing for the stale in-memory manifest, and on Windows a running binary's lock never clears without stopping the process. Rejected.
- **Daemon-coordinated stop → CRC-swap → manifest-refresh → restart (chosen)** — the adapter analog of brain self-update; the only option that fixes both the locked-binary failure and the stale-manifest, at the cost of a daemon IPC apply path plus the per-endpoint resident-children registry.


Both ADRs written. Now register the requirements (inactive per rule 5 — not building yet), update CONTEXT.md, and fold translate-proof into the docs. Read the traceable + CONTEXT structures to edit precisely.


Bash

echo "=== traceable-reqs.toml: last REQ entry + tail ==="; tail -20 "C:/Users/decid/Documents/projects/spt-core-v0132/traceable-reqs.toml"
echo "=== CONTEXT.md .spt + adapter terms ==="; grep -nE '^\s*[-*]?\s*`?\.spt`?|message-idle-translation|translation binary|^### |adapter\b' "C:/Users/decid/Documents/projects/spt-core-v0132/CONTEXT.md" 2>&1 | head -20

Read traceable tail and CONTEXT term locations


=== traceable-reqs.toml: last REQ entry + tail ===
title = "A FLOOD of operator input on one brain↔broker connection deadlocks the broker PERMANENTLY (entire broker — no new/existing attach; the controller stays latched because the per-conn handler can't process the detach). ROOT (doyle /diagnose, code-grounded + HITL capture, the v0.13.0 P1 ctrl+V re-open): `serve_attach` processes a whole `NetStreamData` batch of N operator `Input` records in its inner `for rec in decoder.push()` loop, calling `brain.send_effect(op_id, &bytes)` N times WITHOUT returning to `read_event()` — so the brain writes N `KIND_INPUT` frames back-to-back and drains nothing. The broker's single-threaded per-conn handler answers EACH with `send_frame(applied_envelope)` on the SAME conn (B5 exactly-once ack, KNOWN-HAZARDS 7.2). With the brain not reading, the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block too → mutual full-duplex DEADLOCK. Capture pinned it: 11 input frames, write_input 11/11 (P0 holds — the PTY write is fine), ack send START=11 / END=10 (frame #11's applied-ack never returns). Same class as the v0.12.1 L0 two-conn split. Windows Terminal's ctrl+V paste accelerator was the trigger (injects the clipboard as a char-by-char key flood) but the deadlock is generic to ANY input flood, NOT ctrl+V-specific and NOT a P0 (PTY-write) or W1 (output-drain) regression. The applied-ack is load-bearing ONLY for `shellchan` (one-at-a-time spool delivery WAITS on `BrokerEvent::Applied`); `serve_attach` DISCARDS it (the operator/rc path is fire-and-forward, op_id for dedup only, never gates on the ack). FIX (doyle-approved): CONDITIONAL ACK — `InputReq` gains `ack: bool` (serde default = true, N-1-safe: an older brain's input still acks = today's behavior). `serve_attach`'s operator path calls `send_effect_no_ack` (ack=false) → `dispatch_input` writes NO applied frame → the per-conn handler never writes back while servicing the flood → it always drains → no deadlock (cures ANY input flood). `shellchan` keeps `send_effect` (ack=true) and its `Applied`-wait. Exactly-once PRESERVED: the broker still dedups by (session, op_id) at the applied-set regardless of the ack. N-1 caveat: an OLD resident broker (self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inherent KNOWN-HAZARDS 7.9 broker-resident-wire-change class). (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P1b (todlando, the real ship-blocker behind the ctrl+V wedge; doyle gates). doc = KNOWN-HAZARDS 7.19 (the deadlock + the N-1 broker-resident caveat). impl = InputReq.ack (serde default_true) + Brain::send_effect_no_ack (send_effect_inner shared body) + serve_attach operator path -> send_effect_no_ack + dispatch_input gates send_frame(applied) on req.ack. unit = InputReq.ack serde default=true on an absent field (N-1) + dispatch_input emits the applied frame when ack=true and NONE when ack=false + exactly-once on the no-ack path (a replayed/duplicate op_id still dedups at apply_once, no double PTY write). int = REPRO-FIRST (the keystone, the exact gate the wedge needed): a flood of N>pipe-buffer (deterministic, e.g. 64) operator input frames through serve_attach on ONE conn -> PRE-FIX deadlocks (ack-send-START-without-END, handler stops draining; a concurrent attach / the detach cannot be serviced) -> POST-FIX drains all N + the session stays live + a concurrent attach still OPENS. Real broker+brain, NO mocks. doc = KNOWN-HAZARDS entry (the deadlock + the N-1 broker-resident caveat). clippy/traceable/docs-drift."

# ── P1b sibling (operator HITL 2026-06-19): rc scroll broken — capture-stolen wheel must forward to the harness ──
[[requirements]]
id = "REQ-RC-MOUSE-FORWARD"
title = "On Windows, `spt rc` must FORWARD scroll-wheel events to the harness when the harness has mouse reporting on. ROOT (operator HITL): P1's RawGuard EnableMouseCapture (added for right-click paste, REQ-RC-WIN-PASTE) makes Windows Terminal forward ALL mouse — including the scroll wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen by the capture). Operator ruling: keep mouse capture + right-click bracketed paste AND forward scroll to the harness. FIX (doyle design, cfg(windows), folds into the rc mouse handler): TRACK the harness's mouse-reporting mode by scanning its OUTPUT stream for the DECSET set/reset — ESC[?1000h/1002h/1003h (mouse on) + ESC[?1006h (SGR ext) and their ...l (off) — into a shared MouseMode{enabled,sgr} (pump writes from output, stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN -> bracketed clipboard paste (unchanged, REQ-RC-WIN-PASTE); ScrollUp/Down -> translate to an xterm SGR mouse report (ESC[<64;col+1;row+1M up / ESC[<65;..M down, 0-based crossterm -> 1-based xterm) and forward ONLY when enabled && sgr (else DROP — a legacy X10 report the harness may not parse is garbage); Moved/drag/left/middle -> DROP (scroll is the operator's need; click-forward risks garbage, no click-to-position). Unix UNCHANGED (no capture; the terminal scrolls natively). (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 P1b (todlando, operator-ruled scroll fix bundled with the ack-deadlock; doyle gates). NO int (live console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; the pure translator + scan unit-tested). impl = (cfg(windows)) MouseMode{enabled,sgr atomics} + MouseModeScanner (carry-buffer DECSET scan, parse_decset_private/apply_mouse_mode) fed from the pump's output-render path + scroll_dir + scroll_sgr (SGR encode) + the spawn_stdin_reader_events scroll arm (forward when enabled&&sgr) threaded via Arc<MouseMode> (pump writes / reader reads). unit = scroll_dir classify (wheel-only) + scroll_sgr EXACT xterm bytes (up=64/down=65, col+1/row+1) + DECSET scan (1000/1002/1003/1006 set&reset, combined ESC[?1002;1006h, mixed stream, and SPLIT across two feeds — the carry-buffer edge). doc = KNOWN-HAZARDS entry (rc Windows scroll forwarded as SGR when the harness reports mouse; capture is for right-click paste). doyle gates; operator HITL (scroll works in a real CC rc session; right-click still pastes)."

# ── P1c (operator-ruled root-fix before v0.13.0 ship; doyle root-cause via instrumented repro on kitsubito, 2026-06-20): controller-writer reorder on brain-restart re-serve ──
[[requirements]]
id = "REQ-HAZARD-CONTROLLER-WRITER-REORDER"
title = "Two `controller_writer` threads must never race ONE brain↔broker connection's socket. ROOT (doyle, instrumented RACEDIAG repro on kitsubito): on a brain-restart re-serve the handoff brain registers as controller on the SAME session TWICE over the SAME `Brain::conn` socket — (1) `Brain::handoff` eagerly `subscribe(prior.session_id, prior.next_seq=1)` → `become_controller(from_seq=1)`, initial=[1], spawns writer-A (writes seq 1); (2) `serve_attach` re-handles the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, initial=[0,1], spawns writer-B (writes 0 then 1). `become_controller` (broker.rs) drops the prior `ControllerSink` (its `tx`) but does NOT stop the prior writer thread — writer-A keeps flushing its owned `initial` batch, and both writers hold clones of the same `SharedSend` (`Arc<Mutex<socket>>`) with NO inter-thread ordering. When writer-A's seq 1 wins the socket before writer-B's seq 0, the strict legacy consumer (brain.rs read_event reject-gap path) sees `output gap: got seq 1 want 0` → the test `attach_survives_target_brain_restart_exactly_once` panics at `.expect(\"re-serve\")` OR HANGS in `render_until` (serve thread died on the gap → MARKER_TWO never reaches the wire). `prior.next_seq` is life1's CONSUMPTION cursor, NOT life2's connection state — life2's socket has been sent NOTHING, so a `from_seq=0` full replay on a connection that already streamed seq 1 is contradictory. Snap-above tolerance ALONE can't fix it (it would dedup-drop the late seq 0 → byte loss → the exactly-once byte-identity assert fails). PRE-EXISTING, surfaced by the v0.13.0 green-both-runners release gate; P1b is INNOCENT (its diff touches only input-ack machinery, proven mechanically + the test passes post-P1b in isolation). Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`. INVARIANT: on a single brain↔broker connection the controller output-frame stream is monotonic non-decreasing in seq (modulo dedup re-sends); exactly ONE `controller_writer` is ever live per connection; a SUPERSEDED writer writes NO further frames; a re-serve never replays a seq below what the connection already received. FIX (doyle design, corrected at the gate 2026-06-20): fix #1 as designed ('drop handoff's eager subscribe so serve_attach's attach_as is the sole registration') was REVERTED — handoff's `subscribe(prior.next_seq)` IS the standalone-resume mechanism (the brain-only update engine `apply_brain_only` + the `handoff`/`idempotent`/`daemon_e2e` int tests replay output through it with NO `serve_attach`; dropping it hung every resume-via-handoff test). The shipped fix is three parts: (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs the production dedup-below+snap-above path, never the strict reject-gap legacy trap; this is COMPLETE (not merely tolerant) because every `controller_writer` emits an ASCENDING seq stream and the surviving writer (serve_attach's attach_as(sid,0)) offers the complete `[0,end]` range, so a snap-above merge of ascending writers delivers `[K,end]` with no skip/dup (first sighting of any seq>M is preceded by M on that writer). (2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded — no check-then-block-then-write window, no superseded replay (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is NOT gated (new output only flows to the current controller; a superseded writer must still deliver its terminal `Displaced` kick — gating it suppressed the loud-take notice; it ends on `tx`-drop). (3) EXPLICIT-RESUME / OPERATOR-STREAM BOUNDARY (the LOAD-BEARING fix — kitsubito RACEDIAG ~33% repro the keystones missed) — `Brain::subscribe_with` (shared by attach/attach_as) resets the resume-mode dedup cursor to `from_seq`. The handoff eager subscribe makes serve_attach's brain receive the replay's seq K BEFORE the operator Request is processed (`attached`=false); that frame is dropped by the if-attached forward gate but the snap-above cursor already advanced past K, and `attach_as(sid,0)`'s re-subscribe used to leave the cursor advanced → the broker's re-send of seq K arrives below it, deduped, never forwarded → operator viewport forward-gap (silent content loss in the real rc consumer). Resetting to from_seq on the attach_as re-subscribe re-delivers from 0 (operator dedups the overlap) so seq K reaches the viewport. The epoch gate (2) is sound (RACEDIAG: zero socket interleaving above K); cold-start brains (empty map — production dispatch serve) keep the legacy next_seq path, so production is unaffected. (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P1c (todlando, the last v0.13.0 ship-blocker — pre-existing flaky resume race that gates green-both-runners; doyle root-caused + designed, doyle gates). doc = KNOWN-HAZARDS entry (the reorder + the single-live-writer-per-connection invariant + the two compounding defects) + CONTEXT note. impl = (1) Brain::handoff KEEPS its eager subscribe(prior.session_id, prior.next_seq) [fix #1 REVERTED — load-bearing resume] AND seeds session_cursors.insert(prior.session_id, prior.next_seq); (2) OutputLog.controller_epoch -> Arc<AtomicU64>, become_controller passes the new epoch + Arc::clone into controller_writer, whose INITIAL-BATCH loop returns when controller_epoch != its snapshot (read UNDER send.lock(), atomically with write_frame) — the live recv loop is NOT gated (delivers a superseded controller's terminal Displaced, ends on tx-drop), become_controller/mark_controller_gone/the ControllerJob epoch read switch to atomic load/fetch_add; (3) Brain::subscribe_with resets session_cursors.insert(session_id, from_seq) when in resume mode (non-empty map), shared by attach/attach_as — the attach_as(0) re-subscribe re-delivers seq K to the operator viewport (the kitsubito operator-stream boundary fix; reset moved out of attach() into subscribe_with). unit = epoch-gated controller_writer: a superseded writer (epoch bumped) writes NOTHING from its initial batch (real broker socket, two become_controller on one connection -> only the latest writer's monotonic stream reaches the wire) + handoff seeds session_cursors (session_cursor(sid) == prior.next_seq) + resubscribes for resume. int = the keystone: deterministically force two become_controller-on-one-connection (handoff-then-reserve ordering) on a real broker+brain, NO mocks -> PRE-FIX the wire reorders (got seq 1 want 0 / gap or byte-drop) -> POST-FIX the wire stays monotonic + byte-exact + the session stays live; PLUS attach_survives_target_brain_restart_exactly_once green (doyle runs it 20x isolated single-threaded timeout-wrapped on Linux/kitsubito — the deterministic RED-on-revert carrier). Full seam sweep (attach/broker/resume/brain_swap/inject_control_wedge — the shared W1 controller-writer model). clippy/traceable/docs-drift."

# ── P1c follow-up SEED (doyle proposed, todlando gated, doyle concurred 2026-06-20): the structural per-connection-floor guard — inactive, activate-don't-pre-fail ──
[[requirements]]
id = "REQ-HAZARD-CONTROLLER-RETAKE-FLOOR"
title = "`become_controller` should STRUCTURALLY refuse a controller re-take whose `from_seq` falls below the connection's already-delivered contiguous floor — making the P1c reorder invariant un-reintroducible by a future caller, not just removed at the one caller. ROOT/SCOPE (doyle proposed, P1c gate dialogue): P1c fixes REQ-HAZARD-CONTROLLER-WRITER-REORDER three ways (handoff single-take + epoch-gate-under-lock + session_cursors seed), removing the one decreasing-floor double-take and bounding any other to already-committed-only. A self-enforcing broker guard would refuse the bad SHAPE outright. BLOCKER: the obvious predicate (`from_seq >= delivered_through`) is UNSAFE because `delivered_through` is SESSION-WIDE (the `Arc<AtomicU64>` on `OutputLog`, shared by all controllers/viewers, advanced monotonic-MAX; `resume_seq` reads it) — a normal fresh-operator `from_seq=0` attach to a producing session legitimately sits below it (full ring replay + consumer dedup-below/snap-above), and monotonic-MAX can't distinguish the hazard (a `seq1`-without-`seq0` write reads as `2`). The structurally-correct guard needs a NEW per-connection contiguous-sent cursor (the true highest-contiguous seq this socket has received) that does not exist today; the guard then refuses a re-take below THAT. Bigger than P1c; no live gap (P1c fully fixes the actual bug). Mint/refine stages when the per-connection cursor is built. (v0.13.0 follow-up, post-ship)"
required_stages = []  # SEED ONLY (rule 5, activate-don't-pre-fail). The shipped fix is covered by REQ-HAZARD-CONTROLLER-WRITER-REORDER; this is the deferred STRUCTURAL guard (DEFERRED.md row). Activate with real stages only when a milestone adds the per-connection contiguous-sent cursor + the become_controller refuse-decreasing-floor guard. Until then NO evidence is owed.
=== CONTEXT.md .spt + adapter terms ===
23:A rebuilt version of today's Claude Code `spt` plugin. It is the **first consumer** built *atop* spt-core and the **acceptance proof** of spt-core v1 (it reaches feature parity with modern SPT while delegating all core functionality to spt-core, primarily via `spt.exe`, with deeper hooks where useful) — but it **lives and builds in its own repository, outside spt-core**. It is a Claude-Code-specific *adapter*: it holds the Claude Code conventions (hooks, slash-commands, skill/plugin layout, `claude` session-invocation). **spt-core itself contains zero Claude Code conventions** — only the harness-agnostic contract the plugin binds to. The only adapter-shaped artifact ever in this repo is a generic mock/test adapter exercising the manifest + `api` contract (PRD R-DOCS-2). The M2 milestone delivers that contract + the lifecycle primitives; building the plugin is downstream work, not M2.
39:A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle delivery into an spt-hosted PTY goes through an opt-in adapter **translation binary** (`[message-idle-translation-binary]`, ADR-0022): a pure stdin→stdout filter spt-core lifecycle-manages — it reads the `<EVENT>` feed on stdin, emits keystroke-commands (`{key}`/`{delay_ms}`/`{text}`) on stdout, and spt-core applies them to the PTY **atomically** (controller input buffered during the sequence, so injection coexists with a live `spt rc`). The v0.11.0 raw `payload+\r` inject is the degenerate no-choreography case.
41:### Deliverable shape
68:**Naming convention:** these inbound, machinery-facing commands are prefixed **`api `/`api-`** (e.g. `spt api bind`, `spt api state`) to distinguish them from the agent-facing verbs an agent invokes directly (`send`, `ring`, `ready`, …). The `api` namespace is the harness/adapter commands-API; the unprefixed namespace is the agent surface.
70:Together: manifest + subcommand surface = the complete harness API. A sidecar-style long-running adapter process speaking a wire protocol is explicitly **deferred** as a possible v2 alternative for harnesses that outgrow the manifest+hooks shape (e.g. need streaming or in-memory state across events). Not built day-one.
72:**adapter manifest header** (`adapter_name` + version compat):
73:Every manifest declares a unified **`adapter_name`** (e.g. `claude-spt`), carried on every `api` invocation too. It is load-bearing: one daemon hosts endpoints from multiple adapters, so it resolves an endpoint's manifest + seams by `adapter_name`; adapter-update ripples target by it; capability/manifest lookup and telemetry key on it. The header also declares the adapter's own version and a **`min_spt_core_version`** — the minimum spt-core the adapter requires. This declaration must be **readable before an adapter update is applied** (it lives in the manifest header / a small metadata file fetched first), so spt-core can verify compatibility / expected supported features *before* committing the update. If installed spt-core < the adapter's `min_spt_core_version`, surface the incompatibility rather than silently breaking; when spt-core self-updates, re-verify adapters still satisfy (coordinate core + adapter updates when needed). This is a distinct compatibility axis from the node↔node/library wire-protocol version (see Workspace & versioning).
76:**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):
77:A named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local overlay file registered beside the adapter — user-authored, **surviving adapter updates** (the safe space for nodewise customization; the manifest file itself is adapter-owned and overwritten by updates). A local profile may not shadow a shipped profile's name (refused at registration). Profiles are never independently versioned (fork-drift, rejected). **Consent floors are tighten-only**: a profile may demand approval where the parent does not, never loosen a parent's `require_approval` floor — a loosening overlay is invalid at registration. CLI: `spt adapter create-profile` / `delete-profile` / `set-string` operate on **local** profiles only. Use cases: per-account variants, extra hook wiring, trust-narrowed shell profiles.
78:_Avoid_: "manifest fork", "child adapter", per-profile versioning.
80:**adapter strings** (ratified 2026-06-11, Gateway grill):
82:A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** profile's `[strings]` (never adapter-shipped files).
84:**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = "skill.md" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversal and absolute paths are refused at registration). A **local** profile's own file pointers resolve against the user-owned local-profile dir, not the adapter-shipped `strings/` (which adapter updates overwrite) — so a local override survives updates; equivalently a local profile may just inline a literal. Pointers are **validated at registration** (fail-fast on an escaping/missing pointer — records nothing) and **read lazily** at `get-string` (live file edits reflect without re-register); a missing/unreadable file at read time **skip-diagnoses** (emits a diagnostic, returns "not set" — never a silent drop or hard error, mirroring `[digest]`).
85:_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).
89:Once-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: "<kw>"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new session, naturally re-arming) — and emits at most **one hint per message**. **Tiebreak when a message matches multiple hints:** scan in declaration order and emit the FIRST match whose hint this session has not yet seen; if the declaration-order-first matching hint is already seen, fall through to the next unseen matching hint (the once-per-session and ≤1-per-message invariants both still hold). Emit nothing only when every matching hint is already seen. Employing the hook is the adapter dev's choice; profiles override/extend `[hints]` by leaf-replace like any section.
90:_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.
92:**adapter update declaration** (manifest field):
94:Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.
97:How the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.
99:### Manifest seams (outbound contract, detailed)


CONTEXT.md here is a dense domain model (not a bare glossary), and it already carries the `gh_release` detail at line 94. I'll add a focused v0.13.2 model paragraph + the new **resident adapter binary** term right after it. Make the edit:


Edit

The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):
A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a missing-my-triple multi-platform archive is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).

**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.
_Avoid_: calling the Psyche loop or an on-demand extractor a "resident" binary; "restart the endpoint" for what is a per-binary cycle.


<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.


92	**adapter update declaration** (manifest field):
93	<!-- [doc->REQ-UPD-9] -->
94	Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.
95
96	**session-invocation declaration** (manifest field, noted for spt-plugin parity):

**adapter update declaration** (manifest field):
<!-- [doc->REQ-UPD-9] -->
Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

**session-invocation declaration** (manifest field, noted for spt-plugin parity):


The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

**session-invocation declaration** (manifest field, noted for spt-plugin parity):

The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):
A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).

**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.
_Avoid_: calling the Psyche loop or an on-demand extractor a "resident" binary; "restart the endpoint" for what is a per-binary cycle.

**session-invocation declaration** (manifest field, noted for spt-plugin parity):


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

The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

**session-invocation declaration** (manifest field, noted for spt-plugin parity):

The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):
A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).

**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.
_Avoid_: calling the Psyche loop or an on-demand extractor a "resident" binary; "restart the endpoint" for what is a per-binary cycle.

**session-invocation declaration** (manifest field, noted for spt-plugin parity):

Each adapter manifest declares how spt-core should *ripple-update the adapter itself* (see Self-update). One of: **file-pull** (a plugin-directory lookup regex + a gh repo for the adapter's latest files — spt-core fetches + swaps), **delegated command** (a binary command the adapter owns, e.g. `claude.exe plugin update` — spt-core invokes it), or **gh_release** (the adapter ships its updates from its own GitHub releases). After initial bootstrap, the plugin no longer self-manages updates; spt-core conducts them. The **gh_release** avenue (since v0.8.0) declares `repo = "user/repo"` (plus an optional release `asset`, default `adapter.spt`, and an optional Ed25519 `signing_key`): spt-core compares the repo's latest GitHub release version against the installed adapter version and, when newer, fetches the release `.spt` (the same archive primitive as `spt adapter add --release`), then re-extracts and re-registers. Trust mirrors first-acquisition — HTTPS + GitHub when no key is declared; when a `signing_key` is declared, the fetched `.spt` is verified **fail-closed** against a detached signature published as a sibling release asset `<asset>.sig` (lowercase-hex Ed25519 over the raw archive bytes), and the new `.spt` is verified against the **installed** manifest's key (key continuity). A bad or missing signature refuses the update — the staged bytes are deleted, never extracted. The gh_release update is driven by the `spt adapter update [name]` command (with no name it sweeps every registered gh_release adapter); the network fetch lives in the CLI, never the daemon.

+**adapter packaging & live update** (v0.13.2; ADR-0024, ADR-0025):

+A `.spt` may be **multi-platform**: shared `manifest.toml` + `strings/` at the root, role binaries under per-target-triple subdirectories (`x86_64-pc-windows-msvc/`, …); install/update extracts the shared root plus only the current node's triple, flattened into `install_dir`, so flat `<install_dir>/<program>` resolution is unchanged. It stays one signed asset (`adapter.spt`, plain-tar or gzip); a multi-platform archive missing the recipient's triple is a typed `NoArtifactForPlatform`. Large adapters may still split per-platform. The `gh_release` fetch transport is **`auto`** by default — the pre-authorized `gh` CLI when available (the path for **private** adapter repos: `gh` honors both OAuth and `GH_TOKEN`, so spt never custodies a token), else direct HTTPS (public). An adapter update is **live and daemon-coordinated**, the adapter analog of brain self-update: for an endpoint with a running **resident adapter binary**, the daemon stops it (releasing the OS file lock that otherwise fails an overwrite on Windows), swaps **only files whose CRC changed**, **refreshes the endpoint's in-memory manifest** (binaries and manifest stay on the same page), then restarts it — the endpoint itself never restarts. An optional `[update].message` is surfaced (markdown-rendered) when an update is actually applied (not on a no-op).

+**resident adapter binary**: an adapter-owned process spt-core keeps alive for an endpoint's lifetime (today the `[message-idle-translation-binary]`), as opposed to **ephemeral** adapter binaries — the Psyche loop (daemon-hosted, ADR-0004), the `[digest]` extractor, `[session.*]` runners, hooks — which spawn on demand and pick up an update on their next invocation. Only resident binaries are stopped/restarted on a live update; ephemerals self-heal.

+_Avoid_: calling the Psyche loop or an on-demand extractor a "resident" binary; "restart the endpoint" for what is a per-binary cycle.

How the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.


CONTEXT.md done. Now register the requirements (rule 3) — the two ADRs carry `[doc->REQ-X]` tags so those REQs must exist; I'll activate their **doc** stage (the ADR is the evidence) and leave impl/unit/int + the smaller additive REQs inactive (rule 5). Read the traceable tail to append cleanly.


1	# traceable-reqs manifest for spt-core — SEED.
2	#
3	# Authoritative requirement registry: every PRD R-* requirement and every
4	# KNOWN-HAZARDS invariant lives here as a REQ-* id (see docs/TRACEABILITY.md
5	# for the development contract).
6	#
7	# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`)
8	# so `check` stays green pre-code. A milestone (M0-M5) ACTIVATES its requirements
9	# by setting their real `required_stages`. Validate this seed against the
10	# installed CLI at M0 (the binary isn't installed yet).
11
12	[scan]

# traceable-reqs manifest for spt-core — SEED.
#
# Authoritative requirement registry: every PRD R-* requirement and every
# KNOWN-HAZARDS invariant lives here as a REQ-* id (see docs/TRACEABILITY.md
# for the development contract).
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`)
# so `check` stays green pre-code. A milestone (M0-M5) ACTIVATES its requirements
# by setting their real `required_stages`. Validate this seed against the
# installed CLI at M0 (the binary isn't installed yet).

[scan]


Confirmed the activation model (inactive until a milestone activates). I'll append the 5 REQs — the 2 ADR-backed ones with `doc` active (the ADR is the evidence), the 3 additive ones inactive — then run `check`. Appending:


Bash

cat >> "C:/Users/decid/Documents/projects/spt-core-v0132/traceable-reqs.toml" <<'EOF'

# ── v0.13.2 adapter packaging + live-update grill (operator-driven, 2026-06-22; doyle captured the design) ──
# doc stage ACTIVE for the two ADR-backed REQs (the ADR IS the doc evidence); impl/unit/int deferred to the
# v0.13.2 build milestone (rule 5). The three additive REQs stay fully inactive until built.
[[requirements]]
id = "REQ-ADAPTER-MULTIPLATFORM-SPT"
title = "A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `<install_dir>/<program>` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2)"
required_stages = ["doc"]  # doc = ADR-0024 + CONTEXT "adapter packaging & live update". impl/unit/int activate at the v0.13.2 build.
[[requirements]]
id = "REQ-ADAPTER-LIVE-UPDATE"
title = "An adapter update is live and daemon-coordinated (the adapter analog of brain self-update, ADR-0004): for an endpoint with a running RESIDENT adapter binary (today the `[message-idle-translation-binary]`), the CLI keeps fetch+verify and hands the APPLY to the daemon over IPC, which per affected endpoint (1) STOPS the resident binary -> releases the OS file lock (fixes the Windows 'Access denied (os error 5)' overwrite failure), (2) swaps on disk ONLY files whose CRC differs from the staged archive (unchanged files + their still-running binaries untouched), (3) RE-CLONES the new on-disk manifest into the running `BrainLifecycle` (the in-memory manifest is cached at bringup and otherwise goes stale -> binaries+manifest back on the same page), (4) RESTARTS the resident binary from the new files. An endpoint NOT running -> CLI swaps directly (no lock, no cache). Only the resident class is cycled; ephemeral adapter binaries (Psyche loop, `[digest]` extractor, `[session.*]` runners, hooks) self-heal on next spawn and are excluded. The daemon keeps a per-endpoint registry of resident adapter children. (ADR-0025, v0.13.2)"
required_stages = ["doc"]  # doc = ADR-0025 + CONTEXT "resident adapter binary". impl/unit/int activate at the v0.13.2 build.
[[requirements]]
id = "REQ-ADAPTER-UPDATE-MESSAGE"
title = "An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's \"run `/reload-plugins` in any ongoing sessions\". (v0.13.2)"
required_stages = []  # additive; activate at the v0.13.2 build.
[[requirements]]
id = "REQ-ADAPTER-GH-TRANSPORT"
title = "The `gh_release` avenue (and `spt adapter add --release`) gain a fetch `transport`: `https` (current reqwest direct, public), `gh` (shell the pre-authorized `gh` CLI — the private-repo path; `gh` honors OAuth and `GH_TOKEN`, so spt custodies no token), or `auto` (default: prefer `gh` when installed+authed, else HTTPS). `--gh`/`--https` force it on `add`. Additive over the existing fetch path; verify->extract->register downstream is unchanged. (v0.13.2)"
required_stages = []  # additive; activate at the v0.13.2 build.
[[requirements]]
id = "REQ-ADAPTER-PROOF-DIR-OVERRIDE"
title = "The author-time proof commands (`spt adapter digest-proof`, `spt adapter translate-proof`) gain a `--dir <path>` / `--manifest <file>` override so an author proofs a DEV binary against an on-disk manifest+install dir WITHOUT staging a full extracted GhReleaseManaged install (mirrors digest-proof's `--sample` pointing straight at a file). Fixes perri F-011: a bare-file-added gh_release adapter currently can't be resolved by the *-proof commands ('manifest is not present yet at <dir>'); un-stales the bare-file digest-proof int. (perri F-011, v0.13.x DX)"
required_stages = []  # additive DX; activate when built.
EOF
echo "appended"; echo "=== run traceable check ==="; cd "C:/Users/decid/Documents/projects/spt-core-v0132" && (cargo run -q -p xtask -- traceable check 2>&1 | tail -15 || traceable-reqs check 2>&1 | tail -15)

Append v0.13.2 REQs and run traceable check


appended
=== run traceable check ===
usage: xtask <gen|check|site <outdir>|release-keygen <key-id>|release-sign …|release-publish …|debug-keygen <key-id>|debug-pin …|debug-rollout …|debug-converge --version <u64> …>
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


xtask has no traceable subcommand — it's the standalone `traceable-reqs` tool. Run it against the worktree.


cd "C:/Users/decid/Documents/projects/spt-core-v0132"; traceable-reqs check 2>&1 | tail -20; echo "EXIT=$?"

Run traceable-reqs check in worktree


  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
  [OK] REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
EXIT=0
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


`traceable-reqs check` **EXIT=0** — green, including the two doc-active ADR REQs (the ADR doc-tags resolve). Now the translate-proof docs gap: read how digest-proof appears in the checklist + patterns, mirror it.


cd "C:/Users/decid/Documents/projects/spt-core-v0132"; echo "=== integration-checklist.md (digest-proof context) ==="; grep -n -B2 -A2 "digest-proof" docs-site/src/harness-contract/integration-checklist.md | head -30; echo "=== patterns.md (digest-proof context) ==="; grep -n -B2 -A4 "digest-proof" docs-site/src/harness-contract/patterns.md | head -40

Read digest-proof entries in checklist + patterns


=== integration-checklist.md (digest-proof context) ===
86-| **`api presence <id>` / `api driven-by <id>`** | Most-recently-active resolution across the subnet; lets a session tell local input from remote-drive | RUN |
87-| **Workers** (`api worker-start <parent> <id>`, `worker-poll`, `worker-stop`) | Nested, short-lived sub-agents under a parent endpoint | RUN |
88:| **`[digest]` extractor** (or `api digest-entry`) | A live activity digest (`spt endpoint digest`) — declare an extractor mapping your native log → the `{role, text, tool, ts}` contract (ADR-0019; its OWN seam, no longer riding `[history]`). Spans `/clear` via the session ledger; validate with `spt adapter digest-proof` | RUN |
89-| **`[session.notif]` template** | Native OS notification render (toast / shell alert) for consent + capability prompts, instead of burying them in agent output | RUN |
90-| **`[session.resume]` template** (spt-hosted) | The **native-resume** sibling of `[session.self]`: spt-core picks it over `[session.self]` when a bringup carries a prior session (`spt endpoint run --resume`, or the picker's *Resume from history*). Declare your harness's native-resume verb (e.g. `claude -r {session_id}`) — **skip it and a resume re-runs the fresh command → a blank transcript.** spt-core lands the PTY in the session's recorded project cwd (a harness resolves a transcript by `session_id` + cwd) | START |
--
143-- [ ] An inbound delivery channel is declared (`[inject]`) or pulled (`api poll`)
144-- [ ] `[history]` strategy chosen; `api boundary` wired for clear/compact
145:- [ ] (for a live digest) `[digest]` extractor declared + `digest-proof`-checked, or `api digest-entry` push
146-- [ ] (spt-hosted, if your harness resumes by id) `[session.resume]` declares the native-resume command — else a resume comes up blank
147-- [ ] (spt-hosted, for idle message delivery) `[message-idle-translation-binary]` declared, or accept the degenerate `payload+enter` inject
=== patterns.md (digest-proof context) ===
175-  and spt-core keeps the key catalog harness-agnostic. Handle both shapes: `--in`
176-  a directory (locate the session) and `--in` a direct file (the
177:  `digest-proof --sample` path).
178-- **Resolve a runtime-relocated state tree in the binary.** When a runtime value
179-  (an env var, an isolated profile) moves the real transcript tree, have the
180-  extractor prefer that value on its directory branch, with the manifest `source`
181-  as the fallback root. That resolution is logic, so it lives in the binary — the
--
188-  part of why this seam is a binary.)
189-
190:Prove the whole path with `spt adapter digest-proof <adapter> --sample <file>`
191-(below).
192-
193-## The bringup / launcher seam
194-
--
322-    bringup spawns. (A clean `add` already proves the cross-field shape, since add
323-    is manifest-first; `capability` is the lighter, non-mutating check.)
324:  - `spt adapter digest-proof <a> --sample <file>` runs the real extractor
325-    through the registry and renders the result — proving the
326-    transcript → record → render path end-to-end on a fixed sample. It fills the
327-    same runtime substitution keys the daemon does, so passing proof means it
328-    works at runtime. (Use a recent `spt` — current binaries fill the full key
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


Clear placement. Let me get the exact table row for `[message-idle-translation-binary]` (to mirror the digest row's "validate with…") + the patterns proof-list block.


cd "C:/Users/decid/Documents/projects/spt-core-v0132"; echo "=== checklist: translation-binary table row ==="; grep -n "message-idle-translation" docs-site/src/harness-contract/integration-checklist.md; echo "=== patterns proof-list (318-335) ==="; sed -n '318,335p' docs-site/src/harness-contract/patterns.md

Find translation-binary table row + patterns proof-list


<owl_messages>
<owl_message from="perri" priority="high">Adapter shipped: claude-spt v0.6.0 is published — GitHub release on SaberMage/spt-claude-code (tag v0.6.0), carrying both v0.13.0 seams ([session.resume] + [message-idle-translation-binary], the {commit}-terminated binary). Per-OS assets attached: adapter-windows-x86_64.spt + adapter-linux-x86_64.spt (real ELF, zig cross-built) + a default adapter.spt (windows copy, the F-014 stopgap). End users on spt >=0.13.0 get idle message delivery + session resume via `spt adapter add --release SaberMage/spt-claude-code` / `spt adapter update`.

So the whole loop is closed end-to-end and public: spt-core v0.13.1 (your seams + the contract fix + translate-proof) ⟷ claude-spt v0.6.0 (the adapter wiring). Deferred-as-tracked: the resume real-bringup int + the idle-translate APPLY-half int (both real-claude tier), and the v0.13.x adapter-DX batch (F-011 --dir/--manifest override). Ping me when that batch lands or if the operator wants the resume gate. Good run.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


=== checklist: translation-binary table row ===
91:| **`[message-idle-translation-binary]`** (spt-hosted) | A lifecycle-managed `stdin→stdout` JSON-lines binary that turns inbound `<EVENT>` messages into keystroke-commands spt-core applies to the PTY **atomically** (coexists with a live `spt rc` controller). The agnostic way to deliver messages into an **idle** spt-hosted session; busy delivery stays your `[inject]` hook path | RUN |
101:declares a `[message-idle-translation-binary]` (`cc-spt-idle-translate`) so inbound
147:- [ ] (spt-hosted, for idle message delivery) `[message-idle-translation-binary]` declared, or accept the degenerate `payload+enter` inject
=== patterns proof-list (318-335) ===
  registry.
- Two author-time tools work without a live session:
  - `spt api --adapter <a> --manifest <file> capability` reports the manifest's
    hostable types from the manifest alone — assert it advertises the type your
    bringup spawns. (A clean `add` already proves the cross-field shape, since add
    is manifest-first; `capability` is the lighter, non-mutating check.)
  - `spt adapter digest-proof <a> --sample <file>` runs the real extractor
    through the registry and renders the result — proving the
    transcript → record → render path end-to-end on a fixed sample. It fills the
    same runtime substitution keys the daemon does, so passing proof means it
    works at runtime. (Use a recent `spt` — current binaries fill the full key
    map.)

And the meta-lesson: **observable behaviour of the public binary is itself public
surface.** When prose docs lag, a byte-capture against the live `api` / `adapter`
surface is a legitimate way to confirm a contract.

## Next
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


perri shipped **claude-spt v0.6.0** — and it's directly relevant to this grill. Acking her (high-priority), because her **F-014 stopgap is exactly what v0.13.2 #1 fixes.**


$OWL send perri <<'EOF'
Adapter shipped — loop closed end-to-end + public, congrats. claude-spt v0.6.0 ⟷ spt-core v0.13.1 is the full parity proof.

Big relevance: your F-014 stopgap (a default adapter.spt that's just the WINDOWS copy, plus separate adapter-windows-x86_64.spt / adapter-linux-x86_64.spt) is EXACTLY the gap I'm grilling the operator on right now for v0.13.2. The design (ADR-0024, just drafted): a fat MULTI-PLATFORM adapter.spt — shared manifest.toml + strings/ at the root, binaries under per-target-triple subdirs (x86_64-pc-windows-msvc/, x86_64-unknown-linux-gnu/); `add --release`/`update` extracts only the current node's triple, flattened. So a Linux user's flag-free `add --release SaberMage/spt-claude-code` pulls the ONE adapter.spt and gets the LINUX binary — no windows-copy stopgap, no per-OS --asset juggling. Your per-OS split assets stay supported as the opt-in "split" path for large adapters.

I'm citing F-014 as the motivating real-world case in ADR-0024. One naming note I'll fold in: your split assets are adapter-<os>-<arch>.spt; I'll align the ADR's split-path naming so the convention matches what you already shipped. When v0.13.2 lands you can drop the F-014 stopgap entirely.

Tracked for you unchanged: resume real-bringup int + idle-translate APPLY-half int (deferred), and the v0.13.x adapter-DX batch (F-011 --dir/--manifest). I'll ping when the batch or v0.13.2 lands. Good run.
EOF

Ack perri adapter ship + F-014↔v0.13.2 connection


➜ SENT:perri


Now fold F-014 into ADR-0024 as the motivating case + reconcile the split-asset naming with what perri shipped. Editing the ADR:


Edit

A cross-platform adapter (e.g. a harness integration shipping a Windows `.exe` and a Linux ELF for its translation/digest/psyche binaries) cannot ship one artifact.

A cross-platform adapter (e.g. a harness integration shipping a Windows `.exe` and a Linux ELF for its translation/digest/psyche binaries) cannot ship one artifact. This is not hypothetical: claude-spt v0.6.0 hit it (finding **F-014**) and shipped a stopgap — separate `adapter-windows-x86_64.spt` / `adapter-linux-x86_64.spt` assets plus a default `adapter.spt` that is merely a copy of the Windows archive, so a Linux user's flag-free `spt adapter add --release` silently installs Windows binaries.