diff --git a/crates/spt/src/cli.rs b/crates/spt/src/cli.rs
index 680d7ab..5f9fe96 100644
--- a/crates/spt/src/cli.rs
+++ b/crates/spt/src/cli.rs
@@ -523,6 +523,16 @@ enum AdapterCmd {
         /// archive whose root holds manifest.toml + strings/ + binaries).
         #[arg(long)]
         asset: Option<String>,
+        /// Force the `gh` CLI transport for --release (the private-repo path; gh
+        /// honors OAuth + GH_TOKEN, so spt custodies no token). Mutually
+        /// exclusive with --https. Default: auto (gh when installed+authed, else
+        /// HTTPS).
+        #[arg(long, conflicts_with = "https")]
+        gh: bool,
+        /// Force direct HTTPS transport for --release (public repos). Mutually
+        /// exclusive with --gh. Default: auto.
+        #[arg(long)]
+        https: bool,
     },
     /// Soft-deregister: hidden from new-creation/discovery; existing and live
     /// instances keep running. The manifest's optional `uninstall` template
@@ -2552,6 +2562,140 @@ fn http_get_bytes(client: &reqwest::blocking::Client, url: &str) -> Result<Vec<u
     resp.bytes().map(|b| b.to_vec()).map_err(|e| e.to_string())
 }

+/// A blocking reqwest client stamped with the spt user-agent, shared by every
+/// release HTTPS fetch (asset bytes, version JSON, detached signature).
+fn release_http_client() -> Result<reqwest::blocking::Client, String> {
+    reqwest::blocking::Client::builder()
+        .user_agent(concat!("spt/", env!("CARGO_PKG_VERSION")))
+        .build()
+        .map_err(|e| e.to_string())
+}
+
+/// The transport actually used for a release fetch, after `Auto` is resolved.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum EffectiveTransport {
+    Https,
+    Gh,
+}
+
+/// Resolve a declared [`Transport`](spt_runtime::manifest::Transport) to the
+/// concrete avenue. `Auto` consults `gh_available` (a bounded probe) — `gh` when
+/// installed+authed, else HTTPS. Pure given the probe, so the decision is
+/// unit-testable with an injected closure.
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
+fn resolve_transport(
+    declared: spt_runtime::manifest::Transport,
+    gh_available: impl FnOnce() -> bool,
+) -> EffectiveTransport {
+    use spt_runtime::manifest::Transport;
+    match declared {
+        Transport::Https => EffectiveTransport::Https,
+        Transport::Gh => EffectiveTransport::Gh,
+        Transport::Auto => {
+            if gh_available() {
+                EffectiveTransport::Gh
+            } else {
+                EffectiveTransport::Https
+            }
+        }
+    }
+}
+
+/// Bounded probe: is the `gh` CLI installed AND authenticated? `gh auth status`
+/// exits 0 only when a host is logged in. Bounded (REQ-HAZARD-SUBPROCESS-TIMEOUT);
+/// any spawn error / non-zero / timeout ⇒ unavailable (fall back to HTTPS).
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
+fn gh_available() -> bool {
+    matches!(
+        spt_runtime::run_bounded_command(
+            "gh auth status",
+            &std::collections::BTreeMap::new(),
+            Duration::from_secs(10),
+            None,
+        ),
+        Ok(out) if out.success()
+    )
+}
+
+/// Build the `gh api` command (template + keys) that prints `repo`'s latest
+/// release tag to stdout. Pure — the caller runs it bounded. The `gh` CLI honors
+/// OAuth + `GH_TOKEN`, so this reads a private repo with no token in spt's hands.
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
+fn gh_version_command(repo: &str) -> (String, std::collections::BTreeMap<String, String>) {
+    (
+        "gh api repos/{repo}/releases/latest --jq .tag_name".to_string(),
+        std::collections::BTreeMap::from([("repo".to_string(), repo.to_string())]),
+    )
+}
+
+/// Build the `gh release download` command (template + keys) that writes `asset`
+/// from `repo` into `out_dir`. A concrete `tag` downloads that release; `None`
+/// downloads the latest (gh's no-tag-arg form — `latest` is NOT a literal tag).
+/// Pure — the caller runs it bounded. (REQ-ADAPTER-GH-TRANSPORT)
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
+fn gh_download_command(
+    repo: &str,
+    tag: Option<&str>,
+    asset: &str,
+    out_dir: &std::path::Path,
+) -> (String, std::collections::BTreeMap<String, String>) {
+    let mut keys = std::collections::BTreeMap::from([
+        ("repo".to_string(), repo.to_string()),
+        ("asset".to_string(), asset.to_string()),
+        ("dir".to_string(), out_dir.to_string_lossy().into_owned()),
+    ]);
+    let template = match tag {
+        Some(t) => {
+            keys.insert("tag".to_string(), t.to_string());
+            "gh release download {tag} --repo {repo} --pattern {asset} --dir {dir} --clobber"
+                .to_string()
+        }
+        None => {
+            "gh release download --repo {repo} --pattern {asset} --dir {dir} --clobber".to_string()
+        }
+    };
+    (template, keys)
+}
+
+/// Fetch a release asset's raw bytes via the resolved transport. HTTPS does a
+/// direct GET of the origin asset URL; `gh` shells `gh release download` into
+/// `scratch_dir` (the private-repo path) and reads the file back. `tag` is the
+/// concrete tag (e.g. `v0.13.2`) or `None` for the latest release.
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
+fn fetch_release_asset_bytes(
+    repo: &str,
+    tag: Option<&str>,
+    asset: &str,
+    transport: EffectiveTransport,
+    scratch_dir: &std::path::Path,
+) -> Result<Vec<u8>, String> {
+    match transport {
+        EffectiveTransport::Https => {
+            let url = origin_asset_url(repo, tag, asset);
+            http_get_bytes(&release_http_client()?, &url)
+        }
+        EffectiveTransport::Gh => {
+            std::fs::create_dir_all(scratch_dir).map_err(|e| e.to_string())?;
+            let (template, keys) = gh_download_command(repo, tag, asset, scratch_dir);
+            match spt_runtime::run_bounded_command(
+                &template,
+                &keys,
+                Duration::from_secs(300),
+                None,
+            ) {
+                Ok(out) if out.success() => {
+                    let path = scratch_dir.join(asset);
+                    let bytes = std::fs::read(&path).map_err(|e| e.to_string());
+                    let _ = std::fs::remove_file(&path);
+                    bytes
+                }
+                Ok(out) => Err(format!("gh release download exit {:?}", out.status_code)),
+                Err(e) => Err(e.to_string()),
+            }
+        }
+    }
+}
+
 /// Why a `.spt` archive could not be extracted into the adapter install dir.
 /// `NoArtifactForPlatform` is the typed multi-platform rejection (ADR-0024,
 /// mirrors ADR-0016's `RejectReason`) — a fat archive that carries other
@@ -2751,11 +2895,13 @@ fn extract_release_archive(
 /// rides the `[update] file_pull` avenue. `Err(code)` carries the process exit
 /// code on any failure (the diagnostic is already printed).
 // [impl->REQ-INSTALL-9]
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
 fn fetch_release_adapter(
     adapters: &std::path::Path,
     spec: &str,
     tag: Option<&str>,
     asset: &str,
+    transport: spt_runtime::manifest::Transport,
 ) -> Result<std::path::PathBuf, i32> {
     let safe = spec.replace(['/', '\\'], "-");
     let github = adapters.join("_github");
@@ -2767,19 +2913,9 @@ fn fetch_release_adapter(
         eprintln!("ADAPTER_RELEASE_FAIL: create {}: {e}", dest.display());
         return Err(1);
     }
-    let url = origin_asset_url(spec, tag, asset);
-    eprintln!("Fetching adapter release asset {asset} from {url}…");
-    let client = match reqwest::blocking::Client::builder()
-        .user_agent(concat!("spt/", env!("CARGO_PKG_VERSION")))
-        .build()
-    {
-        Ok(c) => c,
-        Err(e) => {
-            eprintln!("ADAPTER_RELEASE_FAIL: http client: {e}");
-            return Err(1);
-        }
-    };
-    let bytes = match http_get_bytes(&client, &url) {
+    let effective = resolve_transport(transport, gh_available);
+    eprintln!("Fetching adapter release asset {asset} from {spec} via {effective:?}…");
+    let bytes = match fetch_release_asset_bytes(spec, tag, asset, effective, &github) {
         Ok(b) => b,
         Err(e) => {
             eprintln!("ADAPTER_RELEASE_FAIL: fetch {asset}: {e}");
@@ -5629,7 +5765,16 @@ fn cmd_adapter(action: AdapterCmd) -> i32 {
             release,
             tag,
             asset,
+            gh,
+            https,
         } => {
+            // --gh / --https force the transport (clap guards the conflict);
+            // neither ⇒ auto. [impl->REQ-ADAPTER-GH-TRANSPORT]
+            let transport = match (gh, https) {
+                (true, _) => spt_runtime::manifest::Transport::Gh,
+                (_, true) => spt_runtime::manifest::Transport::Https,
+                _ => spt_runtime::manifest::Transport::Auto,
+            };
             let source = match (path, github, release) {
                 (Some(p), None, None) => std::path::PathBuf::from(p),
                 (None, Some(spec), None) => {
@@ -5669,7 +5814,7 @@ fn cmd_adapter(action: AdapterCmd) -> i32 {
                 }
                 (None, None, Some(spec)) => {
                     let asset = asset.as_deref().unwrap_or("adapter.spt");
-                    match fetch_release_adapter(&adapters, &spec, tag.as_deref(), asset) {
+                    match fetch_release_adapter(&adapters, &spec, tag.as_deref(), asset, transport) {
                         Ok(dest) => dest,
                         Err(code) => return code,
                     }
@@ -5958,9 +6103,12 @@ fn cmd_adapter_update(adapters: &std::path::Path, name: Option<&str>) -> i32 {
         };
         let asset = update.asset.as_deref().unwrap_or("adapter.spt");
         let installed = manifest.adapter.version.as_str();
+        // Resolve the fetch transport once (probes `gh` at most once for `auto`).
+        // [impl->REQ-ADAPTER-GH-TRANSPORT]
+        let transport = resolve_transport(update.transport, gh_available);

         // 1. Latest release version off the GitHub API (no download yet).
-        let latest = match gh_latest_release_version(repo) {
+        let latest = match gh_latest_release_version(repo, transport) {
             Ok(v) => v,
             Err(e) => {
                 eprintln!("ADAPTER_UPDATE_FAIL:{}: latest release: {e}", record.name);
@@ -5985,7 +6133,7 @@ fn cmd_adapter_update(adapters: &std::path::Path, name: Option<&str>) -> i32 {
         // 3. Fetch the release .spt (reuse the REQ-INSTALL-9 primitive). The
         //    fetched archive lands under adapters/_github/<safe> as raw bytes we
         //    can verify BEFORE it is extracted+registered.
-        let staged = match stage_gh_release_archive(adapters, repo, &latest, asset) {
+        let staged = match stage_gh_release_archive(adapters, repo, &latest, asset, transport) {
             Ok(s) => s,
             Err(e) => {
                 eprintln!("ADAPTER_UPDATE_FAIL:{}: fetch: {e}", record.name);
@@ -5999,7 +6147,8 @@ fn cmd_adapter_update(adapters: &std::path::Path, name: Option<&str>) -> i32 {
         //    beside it, or the update is REFUSED (bytes never extracted). With no
         //    key, proceed on HTTPS + GitHub trust (the --release acquisition model).
         if let Some(key_hex) = update.signing_key.as_deref() {
-            if let Err(e) = verify_staged_archive(adapters, repo, &latest, asset, &staged, key_hex)
+            if let Err(e) =
+                verify_staged_archive(adapters, repo, &latest, asset, &staged, key_hex, transport)
             {
                 let _ = std::fs::remove_file(&staged);
                 eprintln!(
@@ -6049,23 +6198,40 @@ fn cmd_adapter_update(adapters: &std::path::Path, name: Option<&str>) -> i32 {
     i32::from(failed)
 }

-/// Query the GitHub API for `repo`'s latest release `tag_name`, returning the
-/// version with any leading `v` stripped (e.g. `v0.2.0` → `0.2.0`). HTTPS +
-/// GitHub trust, same as the `--release` acquisition (REQ-UPD-9).
+/// `repo`'s latest release `tag_name`, leading `v` stripped (e.g. `v0.2.0` →
+/// `0.2.0`). HTTPS reads the public GitHub API; `gh` runs `gh api …/releases/latest
+/// --jq .tag_name` (the private-repo path). (REQ-UPD-9, REQ-ADAPTER-GH-TRANSPORT)
 // [impl->REQ-UPD-9]
-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())
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
+fn gh_latest_release_version(
+    repo: &str,
+    transport: EffectiveTransport,
+) -> Result<String, String> {
+    let tag = match transport {
+        EffectiveTransport::Https => {
+            let url = format!("https://api.github.com/repos/{repo}/releases/latest");
+            let body = http_get_text(&release_http_client()?, &url)?;
+            let v: serde_json::Value = serde_json::from_str(&body).map_err(|e| e.to_string())?;
+            v.get("tag_name")
+                .and_then(|t| t.as_str())
+                .ok_or("release JSON had no tag_name")?
+                .to_string()
+        }
+        EffectiveTransport::Gh => {
+            let (template, keys) = gh_version_command(repo);
+            match spt_runtime::run_bounded_command(
+                &template,
+                &keys,
+                Duration::from_secs(60),
+                None,
+            ) {
+                Ok(out) if out.success() => out.stdout.trim().to_string(),
+                Ok(out) => return Err(format!("gh api exit {:?}", out.status_code)),
+                Err(e) => return Err(e.to_string()),
+            }
+        }
+    };
+    Ok(tag.strip_prefix('v').unwrap_or(&tag).to_string())
 }

 /// Compare two dotted-numeric versions (`a` newer than `b`?). Hand-rolled rather
@@ -6094,25 +6260,22 @@ fn version_is_newer(a: &str, b: &str) -> bool {

 /// Fetch a release `.spt` asset's raw bytes to a staging file under the durable
 /// `_github` home (NOT yet extracted/registered), so they can be verified before
-/// trust. Returns the staged archive path. (REQ-UPD-9, reuses the REQ-INSTALL-9
-/// origin URL + HTTP primitives.)
+/// trust. Returns the staged archive path. `transport` selects HTTPS vs the `gh`
+/// CLI (private-repo path). (REQ-UPD-9, REQ-ADAPTER-GH-TRANSPORT)
 // [impl->REQ-UPD-9]
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
 fn stage_gh_release_archive(
     adapters: &std::path::Path,
     repo: &str,
     tag: &str,
     asset: &str,
+    transport: EffectiveTransport,
 ) -> Result<std::path::PathBuf, String> {
     let safe = repo.replace(['/', '\\'], "-");
     let github = adapters.join("_github");
     std::fs::create_dir_all(&github).map_err(|e| e.to_string())?;
     let tag_v = format!("v{tag}");
-    let url = origin_asset_url(repo, Some(&tag_v), asset);
-    let client = reqwest::blocking::Client::builder()
-        .user_agent(concat!("spt/", env!("CARGO_PKG_VERSION")))
-        .build()
-        .map_err(|e| e.to_string())?;
-    let bytes = http_get_bytes(&client, &url)?;
+    let bytes = fetch_release_asset_bytes(repo, Some(&tag_v), asset, transport, &github)?;
     let staged = github.join(format!("{safe}.update.spt"));
     std::fs::write(&staged, &bytes).map_err(|e| e.to_string())?;
     Ok(staged)
@@ -6120,27 +6283,29 @@ fn stage_gh_release_archive(

 /// Fail-closed verification of a staged `.spt` against the adapter's declared
 /// Ed25519 `signing_key`: fetch the detached signature published beside the asset
-/// (`<asset>.sig`, lowercase-hex) and verify the staged bytes against it. Any
-/// fetch/parse/signature failure is an error (the caller refuses the update).
-/// (REQ-UPD-9)
+/// (`<asset>.sig`, lowercase-hex) and verify the staged bytes against it. The
+/// signature rides the SAME `transport` as the archive, so a private-repo `.spt`
+/// stays verifiable (its `.sig` is fetched via `gh` too). Any fetch/parse/
+/// signature failure is an error (the caller refuses the update).
+/// (REQ-UPD-9, REQ-ADAPTER-GH-TRANSPORT)
 // [impl->REQ-UPD-9]
+// [impl->REQ-ADAPTER-GH-TRANSPORT]
 fn verify_staged_archive(
-    _adapters: &std::path::Path,
+    adapters: &std::path::Path,
     repo: &str,
     tag: &str,
     asset: &str,
     staged: &std::path::Path,
     key_hex: &str,
+    transport: EffectiveTransport,
 ) -> Result<(), String> {
     let key = spt_daemon::parse_verifying_key(key_hex).map_err(|e| e.to_string())?;
     let bytes = std::fs::read(staged).map_err(|e| e.to_string())?;
     let tag_v = format!("v{tag}");
-    let sig_url = origin_asset_url(repo, Some(&tag_v), &format!("{asset}.sig"));
-    let client = reqwest::blocking::Client::builder()
-        .user_agent(concat!("spt/", env!("CARGO_PKG_VERSION")))
-        .build()
-        .map_err(|e| e.to_string())?;
-    let sig_hex = http_get_text(&client, &sig_url)?;
+    let github = adapters.join("_github");
+    let sig_bytes =
+        fetch_release_asset_bytes(repo, Some(&tag_v), &format!("{asset}.sig"), transport, &github)?;
+    let sig_hex = String::from_utf8(sig_bytes).map_err(|e| e.to_string())?;
     spt_daemon::verify_detached(&bytes, sig_hex.trim(), &key).map_err(|e| e.to_string())
 }

@@ -7695,6 +7860,165 @@ mod tests {
         Cli::try_parse_from(args)
     }

+    // ── REQ-ADAPTER-GH-TRANSPORT (v0.13.2 W2): gh transport for private adapter
+    // release repos. Covers the DECISION (resolve_transport) and the COMMAND
+    // SHAPE (the gh command builders) only — the real `gh`/network path is HITL
+    // (int), explicitly unit-covered-not-int per doyle.
+
+    // [unit->REQ-ADAPTER-GH-TRANSPORT] An EXPLICIT transport is honored verbatim
+    // and NEVER consults the probe: a `gh_available` closure that panics if
+    // called proves `Https`→Https and `Gh`→Gh short-circuit before any probe.
+    #[test]
+    fn resolve_transport_explicit_never_probes() {
+        use spt_runtime::manifest::Transport;
+        assert_eq!(
+            resolve_transport(Transport::Https, || unreachable!("Https must not probe gh")),
+            EffectiveTransport::Https,
+            "explicit Https resolves to Https without probing",
+        );
+        assert_eq!(
+            resolve_transport(Transport::Gh, || unreachable!("Gh must not probe gh")),
+            EffectiveTransport::Gh,
+            "explicit Gh resolves to Gh without probing",
+        );
+    }
+
+    // [unit->REQ-ADAPTER-GH-TRANSPORT] AUTO consults the probe: gh-when-available
+    // (probe→true ⇒ Gh) else HTTPS (probe→false ⇒ Https). This is the default
+    // (`Transport::Auto`) avenue selection.
+    #[test]
+    fn resolve_transport_auto_follows_probe() {
+        use spt_runtime::manifest::Transport;
+        assert_eq!(
+            resolve_transport(Transport::Auto, || true),
+            EffectiveTransport::Gh,
+            "auto picks gh when the probe reports it available",
+        );
+        assert_eq!(
+            resolve_transport(Transport::Auto, || false),
+            EffectiveTransport::Https,
+            "auto falls back to https when gh is unavailable",
+        );
+    }
+
+    // [unit->REQ-ADAPTER-GH-TRANSPORT] The version command is the exact `gh api`
+    // form that prints the latest tag, with the single `{repo}` substitution key.
+    #[test]
+    fn gh_version_command_shape() {
+        let (template, keys) = gh_version_command("Owner/repo");
+        assert_eq!(
+            template, "gh api repos/{repo}/releases/latest --jq .tag_name",
+            "version template is the gh-api latest-tag form",
+        );
+        assert_eq!(
+            keys,
+            std::collections::BTreeMap::from([("repo".to_string(), "Owner/repo".to_string())]),
+            "version keys carry only {{repo}}",
+        );
+    }
+
+    // [unit->REQ-ADAPTER-GH-TRANSPORT] WITH a concrete tag: the download template
+    // carries the `{tag}` token and every key (tag/repo/asset/dir) resolves to its
+    // argument — `dir` mirroring out_dir's lossy string.
+    #[test]
+    fn gh_download_command_with_tag_shape() {
+        let out_dir = std::path::Path::new("/tmp/x");
+        let (template, keys) =
+            gh_download_command("Owner/repo", Some("v0.13.2"), "adapter.spt", out_dir);
+        assert!(
+            template.contains("{tag}"),
+            "tagged download template carries the {{tag}} token: {template}",
+        );
+        assert_eq!(
+            template,
+            "gh release download {tag} --repo {repo} --pattern {asset} --dir {dir} --clobber",
+            "tagged download template is the exact `gh release download <tag>` form",
+        );
+        assert_eq!(keys.get("tag").map(String::as_str), Some("v0.13.2"), "tag key");
+        assert_eq!(keys.get("repo").map(String::as_str), Some("Owner/repo"), "repo key");
+        assert_eq!(keys.get("asset").map(String::as_str), Some("adapter.spt"), "asset key");
+        assert_eq!(
+            keys.get("dir").map(String::as_str),
+            Some(out_dir.to_string_lossy().as_ref()),
+            "dir key mirrors out_dir's lossy string",
+        );
+    }
+
+    // [unit->REQ-ADAPTER-GH-TRANSPORT] WITHOUT a tag (None ⇒ latest release): the
+    // template has NO `{tag}` token and the keys carry NO "tag" entry — gh's
+    // no-tag-arg form, distinct from passing the literal string "latest".
+    #[test]
+    fn gh_download_command_no_tag_omits_tag() {
+        let out_dir = std::path::Path::new("/tmp/x");
+        let (template, keys) = gh_download_command("Owner/repo", None, "adapter.spt", out_dir);
+        assert!(
+            !template.contains("{tag}"),
+            "latest-release template omits the {{tag}} token: {template}",
+        );
+        assert_eq!(
+            template,
+            "gh release download --repo {repo} --pattern {asset} --dir {dir} --clobber",
+            "latest-release template is the exact no-tag-arg form",
+        );
+        assert!(!keys.contains_key("tag"), "no-tag form carries no `tag` key");
+        // The remaining keys are still fully populated.
+        assert_eq!(keys.get("repo").map(String::as_str), Some("Owner/repo"), "repo key");
+        assert_eq!(keys.get("asset").map(String::as_str), Some("adapter.spt"), "asset key");
+        assert_eq!(
+            keys.get("dir").map(String::as_str),
+            Some(out_dir.to_string_lossy().as_ref()),
+            "dir key mirrors out_dir's lossy string",
+        );
+    }
+
+    // [unit->REQ-ADAPTER-GH-TRANSPORT] The clap force-transport flags parse:
+    // `--gh` alone sets gh=true/https=false; `--https` alone sets https=true; and
+    // `--gh --https` together is a clap error (conflicts_with), so an operator
+    // cannot ask for both transports at once.
+    #[test]
+    fn adapter_add_transport_flags_parse() {
+        // --gh alone.
+        match parse(&["spt", "adapter", "add", "--release", "Owner/repo", "--gh"])
+            .unwrap()
+            .cmd
+            .unwrap()
+        {
+            Cmd::Adapter { action: AdapterCmd::Add { gh, https, .. } } => {
+                assert!(gh, "--gh sets gh=true");
+                assert!(!https, "--gh leaves https=false");
+            }
+            _ => panic!("expected Adapter Add"),
+        }
+        // --https alone.
+        match parse(&["spt", "adapter", "add", "--release", "Owner/repo", "--https"])
+            .unwrap()
+            .cmd
+            .unwrap()
+        {
+            Cmd::Adapter { action: AdapterCmd::Add { gh, https, .. } } => {
+                assert!(https, "--https sets https=true");
+                assert!(!gh, "--https leaves gh=false");
+            }
+            _ => panic!("expected Adapter Add"),
+        }
+        // Default (neither flag): both false → dispatch maps to Transport::Auto.
+        match parse(&["spt", "adapter", "add", "--release", "Owner/repo"])
+            .unwrap()
+            .cmd
+            .unwrap()
+        {
+            Cmd::Adapter { action: AdapterCmd::Add { gh, https, .. } } => {
+                assert!(!gh && !https, "neither flag → both false (auto)");
+            }
+            _ => panic!("expected Adapter Add"),
+        }
+        // --gh --https together is rejected by conflicts_with.
+        assert!(
+            parse(&["spt", "adapter", "add", "--release", "Owner/repo", "--gh", "--https"]).is_err(),
+            "--gh and --https are mutually exclusive (clap conflicts_with)",
+        );
+    }
+
     // [unit->REQ-ENDPOINT-STOP-OFFLINE] `endpoint stop` stamps the perch offline
     // (not merely de-readies it), so a stopped daemon-hosted endpoint reports
     // alive=false — closing the status=online latch that left it reading alive.


W2 diff is strong. Security review of the gh-shelling path:
- **No token custody confirmed** — spt builds command templates carrying only `{repo}/{asset}/{tag}/{dir}`; `gh` reads its own OAuth/`GH_TOKEN`. spt never reads/stores a credential. ✓
- **Injection safety** — reuses `run_bounded_command` (the F-009 tokenize-first-then-fill primitive W1's tar already uses); filled values = one argv element each, no split/quote injection even for a hostile release tag. ✓
- **`.sig` rides same transport** — private signed adapter's `<asset>.sig` fetched via gh too → fail-closed verify still works on private repos. ✓
- **Safe default** — `gh_available` = `gh auth status` exits 0 only when authed; any failure → HTTPS fallback. ✓
- **Probe-once**, explicit-never-probes (proven via `unreachable!()`), clap `conflicts_with` guards `--gh`/`--https`.

Now re-run gates myself:


Bash

cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -3; echo "===CLIPPY EXIT=${PIPESTATUS[0]}==="
echo "===W2 tests==="; cargo test -p spt --bins "resolve_transport" 2>&1 | grep -E "test cli|test result"; cargo test -p spt --bins "gh_" 2>&1 | grep -E "test cli|test result"; cargo test -p spt --bins "adapter_add_transport" 2>&1 | grep -E "test cli|test result"
echo "===spt-runtime schema==="; cargo test -p spt-runtime 2>&1 | grep -E "test result|schema"

Clippy + W2 unit tests + runtime schema


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.70s
===CLIPPY EXIT=0===
===W2 tests===
test cli::tests::resolve_transport_auto_follows_probe ... ok
test cli::tests::resolve_transport_explicit_never_probes ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 219 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test cli::tests::gh_version_command_shape ... ok
test cli::tests::gh_download_command_with_tag_shape ... ok
test cli::tests::gh_download_command_no_tag_omits_tag ... ok
test cli::tests::gh_release_version_compare_decision ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 216 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test cli::tests::adapter_add_transport_flags_parse ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 220 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
===spt-runtime schema===
test manifest::tests::checked_in_schema_is_current ... ok
test manifest::tests::manifest_schema_shape ... ok
test result: ok. 74 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.38s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


All green: clippy EXIT 0, all 6 W2 units pass, spt-runtime 74/74 incl `checked_in_schema_is_current` (schema re-bless correct). Confirm traceable exit:


"C:/Users/decid/AppData/Local/Programs/traceable-reqs/traceable-reqs" check >/tmp/t2.txt 2>&1; echo "TRACEABLE EXIT=$?"; grep -c "\[OK\]" /tmp/t2.txt | xargs echo "OK count:"; grep "REQ-ADAPTER-GH-TRANSPORT" /tmp/t2.txt | head -1

Traceable exit + GH-TRANSPORT verdict


TRACEABLE EXIT=0
OK count: 288
  [OK] REQ-ADAPTER-GH-TRANSPORT  required: [doc, impl, unit]  stages: +doc +impl +unit -int


W1 — Multi-platform .spt packaging (ADR-0024)

REQ-ADAPTER-MULTIPLATFORM-SPT. Fat archive: shared manifest.toml+strings/ at root, binaries under per-target-triple subdirs. Extract shared root + only current_platform() triple, flatten into install_dir. Mode detection on known-triple subdir; legacy flat = back-compat. min_spt_core_version>=0.13.2 forward gate. --asset optional default adapter.spt. Gate: unit+int(real fixture)+doc. Executor builds, doyle gates.

W2 — gh transport for private repos

REQ-ADAPTER-GH-TRANSPORT. transport = https|gh|auto (default auto) on gh_release avenue + add --release. gh shells gh release download/gh api (private path; honors OAuth+GH_TOKEN, no token custody). --gh/--https force. auto = prefer gh if installed+authed else https. Files: fetch path (http_get_bytes, gh_latest_release_version, stage_gh_release_archive), Update struct transport field. Gate: unit (decision+command shape); int HITL/skip. doc=manifest.md+CONTEXT.

W3 — Live daemon-coordinated update + CRC swap (ADR-0025) KEYSTONE

REQ-ADAPTER-LIVE-UPDATE (ADR-0025). W3a resident-children registry; W3b CRC-gated swap (std hash, no new dep); W3c manifest refresh into running BrainLifecycle; W3d daemon-apply IPC (stop resident→swap→refresh→restart; not-running→CLI direct); W3e int keystone NO mocks (real spt-hosted endpoint + running translation binary; Windows locked-binary regression). Bounded stop/restart (join_bounded, never 240s backstop). Brain never restarts. Partial-failure safe.

PSYCHE-LOCK RESOLUTION (doyle design, absorbs task#7 / perri F-010×F-015 — ADR-0025's resident/ephemeral split assumed psyche=in-daemon but it's a separate install-dir binary that locks the target):
- PRIMARY (c): hosted psyche + any daemon-spawned long-lived install-dir binary runs from a PER-ENDPOINT PRIVATE COPY, never the shared install-dir binary → removes the lock vector entirely; consistent with ephemeral-self-heals-on-next-spawn (next spawn copies updated binary).
- SUPPORTING (a): close F-010 orphan reap on endpoint-stop/agent-death (extend v0.12.0 ID-specific scoped-reap to the stop path) — fixes the lingering-orphan leak independent of updates. Check existing psyche-reap REQ before minting new.
- BACKSTOP (b): extract-to-temp + per-file atomic swap so one busy file never fails the whole update (composes with W3b). NOTE (b) alone can't replace a running locked .exe on Windows — (c)/(a) are primary.
W3 int absorbs perri's 4-step repro (task#7) as the NO-mocks acceptance. May need ADR-0025 amendment for the separate-process-psyche vector + possibly a new REQ for the stop-path orphan reap. Author full W3 design brief after W2 gates.

W4 — update-message

REQ-ADAPTER-UPDATE-MESSAGE. [update].message plain multi-line, markdown-rendered (helpfmt prose path), ONLY when update actually applied (version changed), not no-op. Read from newly-installed manifest, avenue-agnostic. Files: Update struct message:Option<String>; cmd_adapter_update after successful apply→helpfmt::render+print. Gate: unit (rendered on applied, NOT no-op). doc=manifest.md+CONTEXT. No int.

W5 — *-proof --dir/--manifest override (F-011)

REQ-ADAPTER-PROOF-DIR-OVERRIDE. --dir <path>/--manifest <file> on digest-proof+translate-proof → proof DEV binary vs on-disk manifest+dir without full extracted install. Files: cmd_adapter_digest_proof + cmd_adapter_translate_proof accept --dir/--manifest, resolve from there not registry::resolve_option. Gate: unit (override resolves dev manifest+dir); int (mirror proof ints with --dir). doc=proof --help+patterns. May ship v0.13.2 or separate batch.

Release — cut v0.13.2 (deployah role)

After all waves gate-pass + CI green BOTH runners: verify release counter against spt-releases metadata at cut (v0.13.1 lesson, expect 29). Author version bump (workspace.package.version 0.13.1→0.13.2 + CHANGELOG section=release body + Cargo.lock first-party lines). Push bump→CI green both→tag→sign+publish. Coordinate publish ping to perri (drops claude-spt F-014 per-OS stopgap; F-011 closed). W3 int + Windows-lock regression = ship-blockers.

W3 design input — adapter psyche binary locks shared install-dir (perri F-010×F-015)

perri live repro 2026-06-22 (spt 0.13.1, claude-spt 0.6.0, Win): claude-spt-psyche.exe (pid 181272, --id wall-a-psyche) ran DETACHED from shared install-dir, parent (152200) DEAD = orphan (~4h), held binary lock → adapter update/add --release failed (tar exit Some(1)). endpoint stop wall-a set alive=false but did NOT reap the detached psyche.

ROOT MECHANICS: psyche is detached per manifest [session.psyche_init] detach=true (Stdio::null, unsupervised), hosted by daemon livehost off the live_agent online status. It runs from the SHARED install dir (lock surface = manifest.toml + claude-spt-digest.exe + claude-spt-psyche.exe + cc-spt-idle-translate.exe + strings/).

LOAD-BEARING ON W3: ADR-0025 resident/ephemeral split assumed psyche=daemon-hosted in-process (ADR-0004), EXCLUDED from cycling. But it's a SEPARATE install-dir binary → unsupervised locker W3's resident-only stop/restart won't release. Fixes compose: (c) psyche-from-own-copy not shared dir = cleanest structural kill, removes vector independent of reap timing; (a) orphan reap on stop/agent-death = F-010 root close; (b) extract-to-temp+atomic-swap = F-015 backstop (one locked file ≠ whole-update fail).

W3 INT FIXTURE (perri spec, deterministic, NO mocks): 1. bring up claude-spt live agent wall-X → daemon spawns detached psyche from install dir; 2. kill psyche PARENT (livehost/agent) → parent-dead orphan; 3. spt endpoint stop wall-X → alive=false BUT psyche survives (reap gap); 4. spt adapter update claude-spt → tar overwrites locked psyche.exe → exit 1 → whole update fails. PASS post-W3: step 3 reaps orphan AND/OR psyche never ran from shared target AND/OR locked-file extract no longer fails whole update. perri re-runs this exact repro as the ship acceptance test on doyle ping.


Traceable EXIT 0, REQ-ADAPTER-GH-TRANSPORT `+doc+impl+unit`. **W2 = GATE-PASS** @0d8f8ca (full diff read, security-reviewed, all gates re-run by me).

Now the W3 keystone. It's the riskiest wave + absorbs perri's psyche-lock finding, so I'll run a **design-gate first** (todlando proposes a code-grounded design; I confirm before he builds the keystone — per the gate-against-documented-design lesson). The brief is large, so I'll write it to a branch doc (avoids owl truncation) and point todlando to it.


Updated task #2 status


Updated task #7 status


Let me ground the brief in real code anchors before writing it (avoid stale references):


echo "=== BrainLifecycle manifest field + lifecycle anchors ==="; grep -rn "struct BrainLifecycle\|manifest:\|ManifestRuntime\|fn.*manifest" crates/spt-daemon/src/lifecycle.rs | head -15
echo "=== psyche spawn / detach / host_one ==="; grep -rn "fn spawn_psyche\|fn host_one\|detach\|psyche_init\|spawn_psyche" crates/spt-daemon/src/livehost.rs | head -15
echo "=== TranslationChild / resident translation binary ==="; grep -rn "TranslationChild\|message-idle-translation\|idle_translation\|fn spawn" crates/spt-daemon/src/translation.rs 2>/dev/null | head -10
echo "=== sha2 / crc dep already present? ==="; grep -rn "sha2\|Sha256\|crc32" crates/spt-daemon/Cargo.toml crates/spt/Cargo.toml

Ground W3 brief in real code anchors


=== BrainLifecycle manifest field + lifecycle anchors ===
28:use spt_runtime::{Manifest, ManifestRuntime};
61:pub struct BrainLifecycle {
62:    runtime: ManifestRuntime,
70:    manifest: Manifest,
83:        manifest: Option<&Manifest>,
93:    pub fn with_config(manifest: &Manifest, id: &str, cfg: DaemonConfig) -> Option<BrainLifecycle> {
101:        manifest: &Manifest,
120:            Some(dir) => ManifestRuntime::with_install_dir(manifest.clone(), dir),
121:            None => ManifestRuntime::new(manifest.clone()),
129:            manifest: manifest.clone(),
599:    // A live-capable manifest: a Psyche init template + a commune dir to watch.
607:    fn live_manifest(commune_dir: &std::path::Path) -> Manifest {
627:    fn live_manifest_with_echo(commune_dir: &std::path::Path) -> Manifest {
=== psyche spawn / detach / host_one ===
99:    /// (`stop_host`) so the detached Psyche process never orphans past its endpoint
234:            // Reap the detached `{id}-psyche` HARNESS PROCESS by handle
346:            continue; // manifest declares no [session.psyche_init]
357:fn host_one(
369:    let psyche_child = match lifecycle.spawn_psyche_owned(Some(&session_id), None) {
373:            // F-010 a successful detached spawn() is NOT a successful host — the
567:/// the first whitespace token of `psyche_init.command`, file-name only, run through
589:/// `psyche_init.command` uses `{id}` (the norm — a Psyche needs its identity;
643:            .psyche_init
770:    /// Register a live-capable adapter `name` (declares `[session.psyche_init]` +
778:             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
829:    // pre-stamped `driven_by = Some("node")` (the lost-detach latch). After the
982:    // [unit->REQ-HAZARD-UNHOST-PSYCHE-REAP] stop_host reaps the detached Psyche
990:    fn stop_host_reaps_the_detached_psyche_process() {
992:            // A real long-lived stand-in for the detached Psyche process.
=== TranslationChild / resident translation binary ===
3://! An adapter may declare a `[message-idle-translation-binary]` (see
19://! **No-zombie invariant (doyle gate #2):** dropping a [`TranslationChild`] closes
222:pub struct TranslationChild {
234:impl TranslationChild {
240:    pub fn spawn(path: &str, cmds: Sender<KeyCmd>) -> io::Result<Self> {
272:        Ok(TranslationChild {
340:impl Drop for TranslationChild {
=== sha2 / crc dep already present? ===
crates/spt-daemon/Cargo.toml:42:sha2 = "0.10"
crates/spt/Cargo.toml:100:# D2 installer-script rung — same sha2 the workspace already carries.
crates/spt/Cargo.toml:101:sha2 = "0.10"


Grounded. Key finding: `stop_host` *already* reaps the detached psyche **by owned handle** (REQ-HAZARD-UNHOST-PSYCHE-REAP) — but perri's orphan survived because its **owning brain died abruptly** (handle lost, stop_host never ran), and the v0.12.0 ID-specific scoped-reap only fires at brain *start*. That's the precise gap for angle (a). `sha2` 0.10 already a workspace dep → W3b uses `Sha256`, no new crate.

Writing the W3 design-gate brief to the branch:


Write

# v0.13.2 W3 — Live daemon-coordinated adapter update: DESIGN-GATE brief

**This is a DESIGN-GATE, not a build order.** W3 is the keystone + the riskiest wave (touches daemon lifecycle just hardened in v0.12.x/v0.13.0) + it absorbs a live field bug (perri F-010×F-015). Per the *gate-against-documented-design* discipline: **return a concrete, code-grounded DESIGN PROPOSAL first — do NOT write impl yet.** doyle confirms the design, then you build.

Authoritative design: **ADR-0025** (`docs/adr/0025-live-daemon-coordinated-adapter-update.md`) + the **W3** section of `V0.13.2-ADAPTER-PACKAGING-JIT.md`. Read both. This brief refines them with a field-bug-driven addendum ADR-0025 does NOT yet cover.

## The field bug that reshapes W3 (perri F-010×F-015, 2026-06-22)

A claude-spt update bricked on Windows: `claude-spt-psyche.exe` was running **detached** (manifest `[session.psyche_init] detach=true`, `Stdio::null`, unsupervised) **straight from the shared install dir**, its parent brain **DEAD** (orphan, lingered ~4h), holding the binary lock → `spt adapter update` / `add --release` failed `extract: tar exit Some(1)`.

**Why this breaks ADR-0025's premise:** ADR-0025 splits adapter binaries into **resident** (cycled on update) vs **ephemeral** (excluded, "self-heal on next spawn"), and classifies the Psyche as ephemeral *because it assumed the Psyche is daemon-hosted in-process (ADR-0004)*. The field proves claude-spt's Psyche is a **separate install-dir binary** — an unsupervised locker that W3's resident-only stop/restart would NOT release. The keystone ships with a hole unless W3 absorbs this.

**Existing lifecycle facts (grounded — confirm in your proposal):**
- `livehost.rs` `stop_host` ALREADY reaps the detached Psyche **by owned handle** (REQ-HAZARD-UNHOST-PSYCHE-REAP, test `stop_host_reaps_the_detached_psyche_process`). It works only on a **graceful** stop with the **owning brain alive**.
- The v0.12.0 **ID-specific scoped-reap** (pid alive AND exe basename == psyche program AND cmdline contains `<id>-psyche`, fail-safe-decline on any unreadable signal) fires only at **brain START**, before the first reconcile.
- perri's orphan slipped BOTH: the owning brain died abruptly (handle lost → `stop_host` never ran), and nothing reaps a brain-less perch's orphan at **endpoint-stop** time.

## The W3 design, three-part psyche-lock fix (doyle's direction — propose the concrete shape)

**(c) PRIMARY — psyche-from-own-copy.** A daemon-spawned **long-lived install-dir binary that runs detached/unsupervised** (today: the Psyche) should execute from a **per-endpoint private copy**, never the shared install-dir binary, so it can never lock the update target — regardless of reap timing. Propose: where the private copy lives (per-endpoint dir), when it's made (at host/spawn), how an update refreshes it (the next spawn copies the new bytes — consistent with "ephemeral self-heals on next spawn"), and **prove `<install_dir>/<program>` resolution (REQ-INSTALL-11) still holds** (the spawn resolves the private-copy path, not the install-dir path). State explicitly whether the **resident translation binary** also gets own-copy treatment or stays in-install-dir (it's resident → W3 stops it → lock releases → it can stay; your call, justify it).

**(a) SUPPORTING — stop-path / brain-death orphan reap.** Extend the v0.12.0 ID-specific scoped-reap to the **endpoint-stop path and brain-death reconcile**, so a brain-less perch's orphan Psyche is reaped (the handle-reap can't, the brain is gone). Keep the fail-safe-decline invariant (a missed dup is bounded; a wrong-kill is catastrophic — never kill on an unreadable signal). This fixes the orphan *leak* independent of updates.

**(b) BACKSTOP — extract-to-temp + atomic per-file swap.** One busy/locked file must not fail the WHOLE update. Compose with W3b's CRC swap. NOTE (b) alone can't replace a *running* locked `.exe` on Windows — that's why (c)/(a) are primary, (b) is the backstop.

## W3 core (ADR-0025) — propose the concrete shape for each

- **W3a resident-children registry.** Where does it live? (the **broker** is the daemon-state anchor across brain restarts — see the broker-is-daemon-state-anchor lesson; justify if you put it elsewhere.) Shape: a per-endpoint record of the live adapter-owned children to cycle (the `TranslationChild` at `translation.rs:240`, + whatever (c) decides about the Psyche). How does `TranslationChild::spawn` feed it; how is it torn down.
- **W3b CRC-gated swap.** Use the **existing `sha2` `Sha256`** (already a workspace dep in spt-daemon + spt — `Cargo.toml:42`/`:101`) for content compare; **no new crc crate**. Replace on disk only files whose hash differs from the staged archive; identical files + their still-running binaries untouched.
- **W3c manifest refresh.** A `BrainLifecycle` method (`lifecycle.rs:61` struct; `manifest: Manifest` field at `:70`, `runtime: ManifestRuntime` at `:62`, built in `with_config` `:93`/`:120-129`) that re-reads the new on-disk manifest and rebuilds BOTH `manifest` and `runtime` (`ManifestRuntime::with_install_dir`). Propose signature + call site.
- **W3d daemon-apply IPC.** CLI keeps fetch+verify (no new daemon HTTP). For an adapter with running resident children, CLI hands **apply** to the daemon over a new IPC command (where on the api/broker IPC surface?). Daemon apply **per affected endpoint**: stop resident children (translation; Psyche per (c)) → W3b swap → W3c refresh → restart resident (Psyche re-spawns from its new own-copy). Endpoint **not** running → CLI swaps directly (no lock, no cache, no round-trip). Route in `cmd_adapter_update` (today the gh_release apply path) + `adapter_update.rs`.
- **Bounded + brain-parity + partial-failure (BINDING).** Stop→restart MUST be bounded (`join_bounded` / kill-bounded; **never ride a 240s backstop**). The endpoint (`BrainLifecycle`/broker) **never restarts** — only the resident child cycles. A mid-swap failure must **not strand** the endpoint (stopped binary + half-swapped files): restart-old / atomic-ish on failure — specify the recovery.

## W3e — int keystone, NO mocks (the ship-blocker)

perri's **exact 4-step repro** is the acceptance (real spt-hosted endpoint + dummy-harness + real daemon, NO mocks):
1. bring up a claude-spt-style live agent wall-X → daemon spawns the detached Psyche; 2. kill the Psyche's PARENT (brain/livehost) → parent-dead orphan; 3. `spt endpoint stop wall-X` → perch alive=false; 4. `spt adapter update` → (pre-fix) tar overwrites the locked binary → `Access denied (os error 5)` / exit 1 → whole update fails.
**PASS post-W3:** step 3 reaps the orphan AND/OR the Psyche never ran from the shared target AND/OR the locked-file extract no longer fails the whole update. The **Windows locked-binary** case is the regression test. Add a graceful-stop variant + a happy-path live-update variant (endpoint stays live, serves the new binary). Reuse the v0.12.1 dummy-harness fixture. If it flakes on the contended runner, use the `SPT_ATTACH_IPC_DEADLINE_MS`/watchdog env-knob precedent.

## Docs + traceability (in your proposal, name what you'll write)

- **ADR-0025 amendment** for the separate-process-Psyche lock vector + the three-part fix (the resident/ephemeral split needs a third axis: *ephemeral-but-install-dir-locking → run from own copy*).
- **REQ activation:** `REQ-ADAPTER-LIVE-UPDATE` → `["doc","impl","unit","int"]`. Decide whether the stop-path/brain-death orphan reap needs its **own** `REQ-HAZARD-*` (likely yes — a distinct lifecycle hazard from the existing brain-start reap) or folds into an existing one; **propose, doyle confirms the REQ shape before you mint.**

## What to return (the DESIGN-GATE packet)

A concrete proposal: the registry location+shape; the (c) own-copy mechanism + REQ-INSTALL-11 proof; the (a) stop-path reap design vs the existing handle-reap; the W3b/W3c/W3d shapes with file:line call sites; the bounded-stop + partial-failure recovery; the W3e fixture plan; the ADR amendment + REQ/REQ-HAZARD plan. **No impl until doyle confirms.** Flag any place ADR-0025 / the JIT is wrong or underspecified.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\W3-DESIGN-GATE.md (file state is current in your context — no need to Read it back)

# v0.13.2 W3 — Live daemon-coordinated adapter update: DESIGN-GATE brief

**This is a DESIGN-GATE, not a build order.** W3 is the keystone + the riskiest wave (touches daemon lifecycle just hardened in v0.12.x/v0.13.0) + it absorbs a live field bug (perri F-010×F-015). Per the *gate-against-documented-design* discipline: **return a concrete, code-grounded DESIGN PROPOSAL first — do NOT write impl yet.** doyle confirms the design, then you build.

Authoritative design: **ADR-0025** (`docs/adr/0025-live-daemon-coordinated-adapter-update.md`) + the **W3** section of `V0.13.2-ADAPTER-PACKAGING-JIT.md`. Read both. This brief refines them with a field-bug-driven addendum ADR-0025 does NOT yet cover.

## The field bug that reshapes W3 (perri F-010×F-015, 2026-06-22)

A claude-spt update bricked on Windows: `claude-spt-psyche.exe` was running **detached** (manifest `[session.psyche_init] detach=true`, `Stdio::null`, unsupervised) **straight from the shared install dir**, its parent brain **DEAD** (orphan, lingered ~4h), holding the binary lock → `spt adapter update` / `add --release` failed `extract: tar exit Some(1)`.

**Why this breaks ADR-0025's premise:** ADR-0025 splits adapter binaries into **resident** (cycled on update) vs **ephemeral** (excluded, "self-heal on next spawn"), and classifies the Psyche as ephemeral *because it assumed the Psyche is daemon-hosted in-process (ADR-0004)*. The field proves claude-spt's Psyche is a **separate install-dir binary** — an unsupervised locker that W3's resident-only stop/restart would NOT release. The keystone ships with a hole unless W3 absorbs this.

**Existing lifecycle facts (grounded — confirm in your proposal):**
- `livehost.rs` `stop_host` ALREADY reaps the detached Psyche **by owned handle** (REQ-HAZARD-UNHOST-PSYCHE-REAP, test `stop_host_reaps_the_detached_psyche_process`). It works only on a **graceful** stop with the **owning brain alive**.
- The v0.12.0 **ID-specific scoped-reap** (pid alive AND exe basename == psyche program AND cmdline contains `<id>-psyche`, fail-safe-decline on any unreadable signal) fires only at **brain START**, before the first reconcile.
- perri's orphan slipped BOTH: the owning brain died abruptly (handle lost → `stop_host` never ran), and nothing reaps a brain-less perch's orphan at **endpoint-stop** time.

## The W3 design, three-part psyche-lock fix (doyle's direction — propose the concrete shape)

**(c) PRIMARY — psyche-from-own-copy.** A daemon-spawned **long-lived install-dir binary that runs detached/unsupervised** (today: the Psyche) should execute from a **per-endpoint private copy**, never the shared install-dir binary, so it can never lock the update target — regardless of reap timing. Propose: where the private copy lives (per-endpoint dir), when it's made (at host/spawn), how an update refreshes it (the next spawn copies the new bytes — consistent with "ephemeral self-heals on next spawn"), and **prove `<install_dir>/<program>` resolution (REQ-INSTALL-11) still holds** (the spawn resolves the private-copy path, not the install-dir path). State explicitly whether the **resident translation binary** also gets own-copy treatment or stays in-install-dir (it's resident → W3 stops it → lock releases → it can stay; your call, justify it).

**(a) SUPPORTING — stop-path / brain-death orphan reap.** Extend the v0.12.0 ID-specific scoped-reap to the **endpoint-stop path and brain-death reconcile**, so a brain-less perch's orphan Psyche is reaped (the handle-reap can't, the brain is gone). Keep the fail-safe-decline invariant (a missed dup is bounded; a wrong-kill is catastrophic — never kill on an unreadable signal). This fixes the orphan *leak* independent of updates.

**(b) BACKSTOP — extract-to-temp + atomic per-file swap.** One busy/locked file must not fail the WHOLE update. Compose with W3b's CRC swap. NOTE (b) alone can't replace a *running* locked `.exe` on Windows — that's why (c)/(a) are primary, (b) is the backstop.

## W3 core (ADR-0025) — propose the concrete shape for each

- **W3a resident-children registry.** Where does it live? (the **broker** is the daemon-state anchor across brain restarts — see the broker-is-daemon-state-anchor lesson; justify if you put it elsewhere.) Shape: a per-endpoint record of the live adapter-owned children to cycle (the `TranslationChild` at `translation.rs:240`, + whatever (c) decides about the Psyche). How does `TranslationChild::spawn` feed it; how is it torn down.
- **W3b CRC-gated swap.** Use the **existing `sha2` `Sha256`** (already a workspace dep in spt-daemon + spt — `Cargo.toml:42`/`:101`) for content compare; **no new crc crate**. Replace on disk only files whose hash differs from the staged archive; identical files + their still-running binaries untouched.
- **W3c manifest refresh.** A `BrainLifecycle` method (`lifecycle.rs:61` struct; `manifest: Manifest` field at `:70`, `runtime: ManifestRuntime` at `:62`, built in `with_config` `:93`/`:120-129`) that re-reads the new on-disk manifest and rebuilds BOTH `manifest` and `runtime` (`ManifestRuntime::with_install_dir`). Propose signature + call site.
- **W3d daemon-apply IPC.** CLI keeps fetch+verify (no new daemon HTTP). For an adapter with running resident children, CLI hands **apply** to the daemon over a new IPC command (where on the api/broker IPC surface?). Daemon apply **per affected endpoint**: stop resident children (translation; Psyche per (c)) → W3b swap → W3c refresh → restart resident (Psyche re-spawns from its new own-copy). Endpoint **not** running → CLI swaps directly (no lock, no cache, no round-trip). Route in `cmd_adapter_update` (today the gh_release apply path) + `adapter_update.rs`.
- **Bounded + brain-parity + partial-failure (BINDING).** Stop→restart MUST be bounded (`join_bounded` / kill-bounded; **never ride a 240s backstop**). The endpoint (`BrainLifecycle`/broker) **never restarts** — only the resident child cycles. A mid-swap failure must **not strand** the endpoint (stopped binary + half-swapped files): restart-old / atomic-ish on failure — specify the recovery.

## W3e — int keystone, NO mocks (the ship-blocker)

perri's **exact 4-step repro** is the acceptance (real spt-hosted endpoint + dummy-harness + real daemon, NO mocks):
1. bring up a claude-spt-style live agent wall-X → daemon spawns the detached Psyche; 2. kill the Psyche's PARENT (brain/livehost) → parent-dead orphan; 3. `spt endpoint stop wall-X` → perch alive=false; 4. `spt adapter update` → (pre-fix) tar overwrites the locked binary → `Access denied (os error 5)` / exit 1 → whole update fails.
**PASS post-W3:** step 3 reaps the orphan AND/OR the Psyche never ran from the shared target AND/OR the locked-file extract no longer fails the whole update. The **Windows locked-binary** case is the regression test. Add a graceful-stop variant + a happy-path live-update variant (endpoint stays live, serves the new binary). Reuse the v0.12.1 dummy-harness fixture. If it flakes on the contended runner, use the `SPT_ATTACH_IPC_DEADLINE_MS`/watchdog env-knob precedent.

## Docs + traceability (in your proposal, name what you'll write)

- **ADR-0025 amendment** for the separate-process-Psyche lock vector + the three-part fix (the resident/ephemeral split needs a third axis: *ephemeral-but-install-dir-locking → run from own copy*).
- **REQ activation:** `REQ-ADAPTER-LIVE-UPDATE` → `["doc","impl","unit","int"]`. Decide whether the stop-path/brain-death orphan reap needs its **own** `REQ-HAZARD-*` (likely yes — a distinct lifecycle hazard from the existing brain-start reap) or folds into an existing one; **propose, doyle confirms the REQ shape before you mint.**

## What to return (the DESIGN-GATE packet)

A concrete proposal: the registry location+shape; the (c) own-copy mechanism + REQ-INSTALL-11 proof; the (a) stop-path reap design vs the existing handle-reap; the W3b/W3c/W3d shapes with file:line call sites; the bounded-stop + partial-failure recovery; the W3e fixture plan; the ADR amendment + REQ/REQ-HAZARD plan. **No impl until doyle confirms.** Flag any place ADR-0025 / the JIT is wrong or underspecified.


Commit the brief to the branch (todlando reads it remotely) + push:


Bash

git add W3-DESIGN-GATE.md && git commit -q -m "$(cat <<'EOF'
docs(w3): live-update design-gate brief (keystone + perri psyche-lock)

DESIGN-GATE for v0.13.2 W3 (ADR-0025): the keystone brief incl. the
F-010×F-015 psyche-lock addendum ADR-0025 doesn't yet cover (the Psyche
is a separate install-dir binary, not in-daemon → an unsupervised locker
the resident-only cycle won't release). Three-part fix: (c) psyche-from-
own-copy primary, (a) stop-path/brain-death orphan reap, (b) extract-to-
temp atomic swap backstop. todlando returns a code-grounded design
proposal; doyle confirms before impl.

Co-authored by: doyle
EOF
)" && git push origin v0.13.2-adapter-packaging 2>&1 | tail -3

Commit + push W3 design-gate brief


warning: in the working copy of 'W3-DESIGN-GATE.md', LF will be replaced by CRLF the next time Git touches it
To https://github.com/SaberMage/spt-core.git
   0d8f8ca..4c1280a  v0.13.2-adapter-packaging -> v0.13.2-adapter-packaging


Updated task #3 owner, status


"$OWL" send todlando doyle <<'EOF'
W2 GATE-PASS @0d8f8ca. I read the full diff + security-reviewed the gh path + re-ran every gate: clippy --workspace --all-targets -D warnings EXIT 0, traceable EXIT 0 (288/288, REQ-ADAPTER-GH-TRANSPORT +doc+impl+unit), all 6 W2 units PASS, spt-runtime 74/74 incl checked_in_schema_is_current. Confirmed: no token custody (spt shells gh, gh holds the cred); injection-safe (run_bounded_command tokenize-first-then-fill, same F-009-safe primitive); .sig rides the same transport (private signed adapter stays verifiable); safe default (gh_available=gh auth status, any failure→HTTPS); clap conflicts_with guards --gh/--https. Clean.

NOW W3 — the KEYSTONE. It's the riskiest wave + it absorbs the perri F-010×F-015 psyche-lock field bug, so I'm running a DESIGN-GATE first: pull the branch and read **W3-DESIGN-GATE.md** (I just committed it @4c1280a, repo root). It refines ADR-0025 with the field-bug addendum ADR-0025 doesn't yet cover (the Psyche is a SEPARATE install-dir binary, not in-daemon → an unsupervised locker the resident-only cycle won't release).

DO NOT WRITE IMPL YET. Return a concrete, CODE-GROUNDED DESIGN PROPOSAL covering: the resident-children registry location+shape; the (c) psyche-from-own-copy mechanism + a REQ-INSTALL-11 resolution proof; the (a) stop-path/brain-death orphan reap vs the existing handle-reap (note: stop_host already reaps by owned handle, but perri's orphan slipped it because the owning brain died abruptly — handle lost; and the v0.12.0 ID-scoped reap fires only at brain START); W3b CRC swap on the EXISTING sha2 Sha256 (no new dep); W3c manifest-refresh BrainLifecycle method (lifecycle.rs:61/70/120-129) signature+call site; W3d daemon-apply IPC command shape; the bounded-stop (join_bounded, never 240s) + partial-failure recovery; the W3e NO-mocks fixture plan (perri's exact 4-step repro + Windows locked-binary regression); the ADR-0025 amendment + whether the stop-path reap needs its own REQ-HAZARD (propose, I confirm the REQ shape before you mint).

The brief has the full detail + grounded anchors. I gate the DESIGN, then you build (likely in reviewable sub-commits W3a→W3e so I can gate incrementally). Flag anything in ADR-0025/the JIT that's wrong or underspecified. Go.
EOF
echo "---sent exit $?---"

Report W2 pass + dispatch W3 design-gate to todlando


➜ SENT:todlando
---sent exit 0---


W3 design-gate dispatched. While todlando designs, check CI on the branch — critical because the daemon-lib lifecycle suite (deadlocks on this contended box) only validates on CI's clean runners:


gh run list --branch v0.13.2-adapter-packaging --limit 6 2>&1 | head -10

CI run status on branch


queued		v0.13.2 — adapter packaging + live update (W1–W5)	ci	v0.13.2-adapter-packaging	pull_request	27986586161	22s	2026-06-22T21:53:56Z
completed	cancelled	v0.13.2 — adapter packaging + live update (W1–W5)	ci	v0.13.2-adapter-packaging	pull_request	27986287729	6m5s	2026-06-22T21:48:08Z
completed	success	v0.13.2 — adapter packaging + live update (W1–W5)	ci	v0.13.2-adapter-packaging	pull_request	27985369653	12m41s	2026-06-22T21:30:40Z


CI picture:
- **W1 state (281176b): CI SUCCESS** both runners (12m41s) — confirms the daemon-lib suite this box can't run.
- **W2**: its run was auto-cancelled (superseded by the W3-brief push); the **queued run @4c1280a covers W1+W2+brief together** — will confirm W2 on both runners (~12min).

**Status:**

| Wave | State |
|------|-------|
| W1 | ✅ GATE-PASS + CI green both runners |
| W2 gh transport | ✅ **GATE-PASS** @0d8f8ca (security-reviewed); CI re-confirming in queued run |
| W3 keystone | 📐 **design-gate dispatched** — todlando returns a code-grounded design proposal (psyche-lock 3-part fix, registry, CRC swap, daemon IPC, NO-mocks fixture); I confirm before he builds |
| W4 / W5 / release | queued |

W3 design-gate doc committed @4c1280a. perri holding for ship-acceptance. Waiting on todlando's design proposal — I gate the design, then incremental build (W3a→W3e). Reachable on `doyle`.


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/12" id="226371c4" type="msg" from="todlando">W3 DESIGN-GATE packet ready @19b58b1 → W3-DESIGN-PROPOSAL.md (repo root, pushed). NO impl, per the gate. 4-agent grounded sweep of the live tree; every claim carries file:line.

THE LOAD-BEARING FINDING (reshapes the wave): the two &quot;resident&quot; things live in OPPOSITE-reachability homes:
- Translat</EVENT-PART>
<EVENT-PART seq="2/12" id="226371c4">ion binary = BROKER state — Translation.child:Arc&lt;TranslationChild&gt; (broker.rs:845) in HostedSession{endpoint} in sessions:Arc&lt;Mutex&lt;HashMap&lt;u64,HostedSession&gt;&gt;&gt; (broker.rs:1112), survives brain restart, IPC-reachable, bounded terminate() (translation.rs:312). → W3a/W3d live here, clean.
- BrainLifecycle (manifes</EVENT-PART>
<EVENT-PART seq="3/12" id="226371c4">t+psyche supervisor) = brain DRIVER-THREAD state — MOVED into the thread closure (livehost.rs:400), NO lock, UNREACHABLE from any IPC handler. → ADR-0025 step 3 &quot;re-clone into the running BrainLifecycle&quot; is NOT implementable as written.

KEY PROPOSALS:
- W3a: don't build a parallel registry — the broker sessions map AL</EVENT-PART>
<EVENT-PART seq="4/12" id="226371c4">READY is the per-endpoint resident registry; add HostedSession{adapter, install_dir} + 2 query helpers.
- W3c: BrainLifecycle.manifest/runtime -&gt; Arc&lt;RwLock&lt;&gt;&gt; (std, no dep); brain REGISTERS the handle into a broker-side per-endpoint map at host; apply swaps via it; pulse loop reads new manifest next tick. Brain-parity kept (</EVENT-PART>
<EVENT-PART seq="5/12" id="226371c4">PTY/broker/OutputLog untouched).
- (c) PRIMARY: detached psyche spawns from a per-endpoint OWN COPY (&lt;perch&gt;/.live-bin/&lt;prog&gt;), copied at spawn from &lt;install_dir&gt;/&lt;prog&gt;; next spawn re-copies new bytes (ephemeral self-heal). REQ-INSTALL-11 proof included (resolution still finds the shipped binary; only the exec path </EVENT-PART>
<EVENT-PART seq="6/12" id="226371c4">is the relocated identical-content copy; args/strings still resolve against install_dir). Resident translation STAYS in-install-dir (supervised → stop-before-swap frees its lock; own-copy unneeded) — justified.
- (a) SUPPORTING: run the EXISTING cmdline-scoped psyche_orphan_should_reap (livehost.rs:595, fail-safe-decline) at the endpoin</EVENT-PART>
<EVENT-PART seq="7/12" id="226371c4">t-stop/brain-death reconcile (280-301/477), not just brain start (683) — fixes perri's brain-less orphan that the handle-reap can't. Propose NEW REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP — YOUR CONFIRM ON ID/SHAPE BEFORE I MINT.
- W3b/(b): reuse spt_daemon::release::sha256_hex (release.rs:603, no new dep); extract-to-temp → per-file CRC</EVENT-PART>
<EVENT-PART seq="8/12" id="226371c4"> diff → atomic write-temp+rename only changed files. Replaces today's remove_dir_all+extract (the Windows os-error-5 path).
- W3d: route in cmd_adapter_update (cli.rs:6064) — none-live → CLI-direct CRC swap; so
...(truncated)</event>
</task-notification>


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


todlando returned the design proposal @19b58b1 (owl truncated at 8/12). The load-bearing finding is exactly what a design-gate is for: **ADR-0025 step 3 "re-clone into the running BrainLifecycle" is NOT implementable as written** — BrainLifecycle is brain driver-thread-local state (livehost.rs:400), unreachable from any IPC handler, while the translation binary is broker state (reachable). Read the full doc:


git pull --ff-only --quiet 2>&1 | tail -1; echo "===HEAD==="; git log --oneline -2 | cat; echo "===PROPOSAL==="; cat W3-DESIGN-PROPOSAL.md

Pull + read full W3 design proposal


===HEAD===
19b58b1 docs(w3): code-grounded design-gate proposal for live adapter update
4c1280a docs(w3): live-update design-gate brief (keystone + perri psyche-lock)
===PROPOSAL===
# v0.13.2 W3 — Live daemon-coordinated adapter update: DESIGN PROPOSAL (todlando → doyle gate)

Response to `W3-DESIGN-GATE.md`. Code-grounded; **no impl written**. All file:line from a 4-agent sweep of the live tree on `v0.13.2-adapter-packaging` @0d8f8ca.

---

## 0. The load-bearing finding (reshapes the whole wave)

W3 touches **two distinct "resident" things that live in two different homes with opposite reachability:**

| Thing | Home | IPC-reachable? | Kill story |
|---|---|---|---|
| **Translation binary** (`[message-idle-translation-binary]`) | **broker** — `Translation.child: Arc<TranslationChild>` (broker.rs:845) inside `HostedSession{endpoint, translation}` in `sessions: Arc<Mutex<HashMap<u64,HostedSession>>>` (broker.rs:872,1112). **Survives brain restart.** | **YES** (broker `handle_conn`) | bounded `terminate()` (translation.rs:312: 500ms grace + 20ms poll + force-kill) |
| **BrainLifecycle** (manifest + psyche supervisor) | **brain driver thread** — *moved* into the `std::thread::spawn(move ...)` closure (livehost.rs:400), behind **no lock**, runs `run_pulse_loop` forever | **NO** — unreachable from any IPC handler; only signal in is `stop: Arc<AtomicBool>` | thread join on stop flag |
| **Psyche** (`[session.psyche_init] detach=true`) | separate **detached install-dir binary**, spawned `spawn_psyche_owned` (psyche.rs:108) → `runtime.spawn_session_owned(PSYCHE_INIT_ROLE)`; handle held as `HostedLife.psyche_child: Option<Child>` (livehost.rs:102) | partially (handle in brain's LiveSet) | `stop_host` handle-reap (livehost.rs:244) **only if brain alive**; else cmdline-scoped reap at **brain start only** (livehost.rs:683) |

**Consequence:** W3a/W3d (cycle the translation child) belong in the **broker** and are clean. W3c (manifest refresh) **cannot** be "re-clone into the running BrainLifecycle" as ADR-0025 states — the live instance is thread-owned and IPC-unreachable. It needs a **shared refresh handle** the brain registers at host time. This is the biggest correction below.

---

## 1. W3a — resident-children registry  → **the broker's `sessions` map already IS it**

Do **not** build a parallel registry (dual source of truth). The broker is the daemon-state anchor (broker.rs:1112, survives brain restart); each `HostedSession` already carries `endpoint: String` (broker.rs:869) + `translation: Option<Arc<Translation>>` (broker.rs:872). The translation child is fed in at session spawn (`dispatch_spawn` → `TranslationChild::spawn`, translation.rs:240; stored broker.rs:845) and torn down on session drop/teardown (`terminate()` at broker.rs:1095/1104 + Drop translation.rs:342).

**Proposed shape:**
- Add two fields to `HostedSession`: `adapter: String` and `install_dir: PathBuf` (sourced from the `SpawnReq`/harnesshost path that already carries `translation_binary`, harnesshost.rs:142). These let an apply ask *"which live sessions run adapter X, and where is its install dir."*
- Add a broker query helper: `fn resident_children_for_endpoint(&self, endpoint: &str) -> Vec<Arc<Translation>>` and `fn endpoints_running_adapter(&self, adapter: &str) -> Vec<(String /*endpoint*/, PathBuf /*install_dir*/)>` over the existing map.
- **Flag (JIT W3a wording):** the JIT says "formalize … into a registry" — grounded, the registry exists; W3a is a thin query + 2 new `HostedSession` fields, not a new structure.

---

## 2. W3c — manifest refresh  → **shared `Arc<RwLock>` handle the brain registers** (ADR-0025 correction)

ADR-0025 step 3 ("Re-clone the new on-disk manifest into the running `BrainLifecycle`") is **not directly implementable**: BrainLifecycle is moved into the brain thread (livehost.rs:400), no lock, IPC-unreachable. Fields read live: `manifest` at lifecycle.rs:355 (`history`/echo); `runtime` at 185/209/430 (`spawn_psyche`, `spawn_psyche_owned`, `fire_echo`).

**Proposed mechanism (std only, no new dep):**
- Change BrainLifecycle fields to interior-mutable shared handles:
  `manifest: Arc<RwLock<Manifest>>`, `runtime: Arc<RwLock<ManifestRuntime>>`.
  The 4 read sites become `.read()` borrows; the refresh becomes `fn refresh_manifest(&self, new: Manifest, install_dir: Option<&Path>)` — now `&self` (interior mutability), so it is callable through a cloned `Arc` from another thread.
- At host time (livehost.rs `host_one`, ~360), the brain **registers** `(endpoint → Arc<RwLock<Manifest>> + Arc<RwLock<ManifestRuntime>>)` into a broker-side per-endpoint map (lives next to / inside the broker, the only IPC-reachable, brain-restart-surviving home). The apply handler swaps via this handle; next pulse tick the brain reads the new manifest.
- **Brain-parity preserved:** the brain thread keeps running; PTY + broker session + OutputLog (all broker-state) untouched; only the manifest/runtime *contents* swap.
- `ManifestRuntime::with_install_dir(manifest, install_dir)` (runtime.rs:298) is the rebuild call (it caches `manifest` + `install_dir` immutably for REQ-INSTALL-11 token resolution; rebuilding = new clone under the write lock).
- **Alternative considered & rejected:** "re-host" (rebuild BrainLifecycle) — heavier, re-spawns the psyche even on a translation-only change, and blurs brain-parity. The `Arc<RwLock>` swap is the minimal correct mechanism.

---

## 3. (c) PRIMARY — psyche-from-own-copy  + REQ-INSTALL-11 proof

The locker is `claude-spt-psyche.exe` running **detached, unsupervised, from the shared install dir** (psyche.rs spawn → `runtime.spawn_session_owned`, resolves `<install_dir>/<program>` via `resolve_program_in_dir`, runtime.rs:524).

**Proposed:** spawn the detached psyche from a **per-endpoint private copy**, never the shared install-dir binary.
- **Where:** a per-endpoint private dir — propose `<perch_dir>/.live-bin/` (per-endpoint, beside the perch, torn down with it). One copy of just the psyche program binary (+ any sibling it needs to exec).
- **When:** at host/spawn (`spawn_psyche_owned`, lifecycle.rs:203 / livehost.rs:369). Resolve the bare psyche token to the shipped binary (`<install_dir>/<program>`), **copy** those bytes to `<.live-bin>/<program>`, then spawn the **copy** path.
- **Update refresh:** the next psyche spawn re-copies the (now updated) install-dir bytes → own-copy self-heals — exactly the "ephemeral self-heals on next spawn" rule, now made lock-safe. On apply: reap the current psyche (handle or §4 scoped-reap), then it re-spawns from a fresh copy of the new bytes.
- **REQ-INSTALL-11 proof:** REQ-INSTALL-11's contract = *a bare program token resolves to the adapter's shipped binary* (so the adapter is self-contained, not PATH-dependent). The own-copy keeps that intact: resolution still **finds** the shipped binary in `install_dir` (to source its bytes); only the **exec path** is the relocated copy, whose *content is identical* to the shipped binary. All other token resolution (args, `strings/`, `{session_id}` keys) still resolves against `install_dir` unchanged. The endpoint stays self-contained; the only change is the running executable no longer holds a lock on the *update target*. Net: REQ-INSTALL-11 holds; we add an explicit `[unit]` proving the spawned psyche path is under `.live-bin/` and its bytes equal `<install_dir>/<program>`.
- **Resident translation binary stays in-install-dir (NOT own-copy).** Justification: own-copy is only needed for an **unsupervised** locker (detached, no handle, reap-timing-dependent). The translation child is **supervised** — broker handle + bounded `terminate()` — so W3d's stop-before-swap releases its lock deterministically. Giving it own-copy would add copy churn for zero benefit. (Stated explicitly per the brief's ask.)

---

## 4. (a) SUPPORTING — stop-path / brain-death orphan reap

Grounded gap: `stop_host` reaps `psyche_child` by handle (livehost.rs:244) **only when the owning brain is alive**; the cmdline-scoped `psyche_orphan_should_reap` (livehost.rs:595 — pid-alive ∧ basename==psyche-prog ∧ cmdline⊇`<id>-psyche`, fail-safe-decline on any unreadable signal) runs **only at brain start** (`reap_orphan_psyches`, livehost.rs:683). perri's orphan slipped both: brain died abruptly (handle lost) and endpoint-stop never ran the scoped reap.

**Proposed:** invoke the **existing** scoped-reap at the **endpoint-stop / brain-death reconcile** path, not just brain start.
- Hook: `reconcile_once` stop-side (livehost.rs:280-301) and `confirm_residency_or_unhost` (livehost.rs:477) — after `set.stop_host(&id)` (handles the handle-alive case), run a scoped-reap pass for `<id>-psyche` to cover the **handle-lost** case (brain-less orphan). Reuse `psyche_orphan_should_reap` verbatim → keep the fail-safe-decline invariant (a missed dup is bounded; a wrong-kill is catastrophic — never kill on an unreadable signal).
- This fixes the orphan **leak** independent of updates (perri step 3 reaps the orphan).
- **REQ-HAZARD: propose a NEW one** — distinct from `REQ-HAZARD-UNHOST-PSYCHE-REAP` (handle reap) and `REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP` (brain-start reap). Proposed id: **`REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP`** — *"endpoint-stop / brain-death reconcile reaps a brain-less perch's orphan detached psyche via the cmdline-scoped guard (the handle-reap cannot — the owning brain is gone), preserving fail-safe-decline."* **doyle confirms the id/shape before I mint.**

---

## 5. (b) BACKSTOP + W3b — CRC-gated, extract-to-temp, atomic per-file swap

Reuse `spt_daemon::release::sha256_hex(&[u8]) -> String` (release.rs:603) — `sha2` already a workspace dep; **no new crate**. Compare = `==` on lowercase hex.

**Proposed swap (replaces today's `remove_dir_all(dest)` + blanket extract — that IS the Windows lock-failure path):**
1. Extract the staged archive to a **temp tree** (sibling of install_dir) — reuses W1 `extract_release_archive` shape, into temp not dest.
2. Walk the temp tree; for each file, `sha256_hex(temp_bytes) != sha256_hex(installed_bytes)` → mark for swap; identical → skip (untouched, incl unchanged running binaries).
3. Swap each differing file **atomically**: write into install_dir as `<file>.new` then rename over the original (rename is atomic on same fs; the stopped-binary's lock is already released by W3d step before this).
4. Remove the temp tree.

This is both W3b (only-changed) and the (b) backstop (one busy file can't fail the whole update — only the file actually being replaced needs its lock free, and W3d frees exactly the resident ones). **Note (per brief): (b) alone can't replace a *running* locked `.exe` — that's why (c)+(a)+W3d-stop are primary.**

---

## 6. W3d — daemon-apply IPC (route + per-endpoint apply + bounded + partial-failure)

**CLI keeps fetch+verify+stage** (`cmd_adapter_update`, cli.rs:6064; stage @6136). New decision after stage:
- **Which endpoints run this adapter live?** `roster::enumerate()` (spt/roster.rs:25) → `.alive` ⨯ `registry::registered()` cross-ref (no live-adapter registry exists; build the snapshot).
  - **None live →** CLI applies directly on the current path **but via the §5 CRC swap** (not `remove_dir_all`) — no lock, no daemon round-trip.
  - **Some live →** CLI hands **apply** to the daemon.
- **IPC surface — the BROKER, not seedmap.** Two surfaces exist: seed-control (`seedmap.rs` — put/take/ping) and the **broker session protocol** (`handle_conn` match, broker.rs:1306; kinds in msg.rs:24+). The apply must reach the translation child + sessions → it is a **broker** command. Add `KIND_ADAPTER_APPLY` (msg.rs) + `AdapterApplyReq { adapter, staged_archive_path, install_dir }` + a `dispatch_adapter_apply` arm at broker.rs:1306 (pattern: `KIND_KILL`→`dispatch_kill`, broker.rs:2184). CLI client sends it over the same broker channel the api client already uses (confirm the exact CLI-side broker-call helper at impl time; `spt/src/api/mod.rs` is the client home).

**`dispatch_adapter_apply`, per affected endpoint (bounded throughout):**
1. Query resident children + manifest-refresh handle (W3a/W3c).
2. **Stop** the resident translation child — `terminate()` (already bounded, translation.rs:312). Reap the psyche (handle or §4 scoped-reap).
3. **§5 CRC swap** install_dir from the staged archive (locks now free).
4. **W3c refresh** the BrainLifecycle manifest/runtime via the shared `Arc<RwLock>` handle.
5. **Restart** the resident translation child from the refreshed manifest path (re-spawn). The psyche re-spawns on the next pulse from a fresh §3 own-copy.

**Brain-parity:** the `BrainLifecycle`/broker session/PTY never restart — only the resident child cycles (translation re-spawn; psyche re-spawn).

**Partial-failure recovery (binding):** ordering = *validate staged → stop child → atomic per-file swap → refresh manifest → restart*. A failure **before** swap-commit leaves install_dir on the OLD bytes → restart the OLD child, report update-failed, endpoint stays live on the old version. Because the swap is per-file write-temp-then-rename (§5), there is no "half-swapped + stopped-binary stranded" window: either a file's rename committed or it didn't; on abort we restart from whatever is on disk (old, since uncommitted renames never landed). The endpoint is **never** left stopped-without-restart.

---

## 7. W3e — int keystone, NO mocks (ship-blocker)

Reuse the v0.12.1 dummy-harness fixture + real broker + real daemon. perri's exact 4-step repro as acceptance:
1. bring up a claude-spt-style live agent `wall-X` → daemon spawns the detached psyche;
2. kill the psyche's **parent** (brain/livehost) → parent-dead orphan;
3. `spt endpoint stop wall-X` → **(a)** scoped-reap kills the orphan; perch `alive=false`;
4. `spt adapter update` → **(c)** psyche never ran from the shared target **and/or (b)** locked-file no longer fails the whole update → extract/swap clean.

**Regression (the gate):** the **Windows locked-binary** case — pre-fix `Access is denied (os error 5)`, post-fix clean.
**Plus two variants:** (i) graceful-stop (handle-reap path still reaps); (ii) happy-path live-update — endpoint stays live, translation stops→CRC-swaps→restarts→**serves the new binary**, psyche re-spawns from the new own-copy.
If it flakes on hfenduleam (heavy: real broker + PTY + resident child), use the `SPT_ATTACH_IPC_DEADLINE_MS` / watchdog env-knob precedent.

---

## 8. Docs + traceability (what I'll write)

- **ADR-0025 amendment** — add the **third axis** to the resident/ephemeral split: *ephemeral-but-install-dir-locking → run from a per-endpoint own copy*. Correct the two errors below; document (a)/(c)/broker-as-registry/manifest-refresh-via-shared-handle.
- **REQ-ADAPTER-LIVE-UPDATE** → `["doc","impl","unit","int"]` (activate at W3 build).
- **New `REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP`** (§4) — doyle confirms id/shape before mint.
- Tags `[impl/unit/int->REQ-ADAPTER-LIVE-UPDATE]` + `[unit/int->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP]` on real evidence.

---

## 9. Flags: where ADR-0025 / the JIT are wrong or underspecified

1. **ADR-0025 mis-classifies the psyche as in-daemon** ("the psyche loop is daemon-hosted, not a separate process (ADR-0004)"). Field-false for claude-spt: it's a separate detached install-dir binary. → §3 own-copy + amendment.
2. **ADR-0025 step 3 ("re-clone into the running BrainLifecycle") is not implementable as written** — BrainLifecycle is thread-moved (livehost.rs:400), unlocked, IPC-unreachable. → §2 shared `Arc<RwLock>` handle registered at host.
3. **JIT W3a ("formalize into a registry")** — the broker `sessions` map already is the per-endpoint resident registry. → §1 thin query + 2 fields, not a new structure.
4. **IPC target ambiguity** — apply is a **broker** command (sessions/translation live there), **not** a seed-control command. → §6.
5. **`TranslationChild::spawn` path is opaque/pre-resolved** (`Command::new(path)`, translation.rs:241, from `SpawnReq.translation_binary`, harnesshost.rs:142). The restart (W3d step 5) must re-resolve the path from the **refreshed** manifest so it picks up new bytes / a moved path.

---

## 10. Proposed build order (reviewable sub-commits, doyle gates each)

`W3a` (broker query + `HostedSession{adapter,install_dir}`) → `W3b` (pure `sha256`-CRC swap helper, unit-tested) → `W3c` (`BrainLifecycle` `Arc<RwLock>` + `refresh_manifest` + brain-registers-handle) → **(a)** stop-path scoped-reap + **(c)** psyche own-copy → `W3d` (`KIND_ADAPTER_APPLY` + `dispatch_adapter_apply` + CLI routing/decision) → `W3e` (NO-mocks int + Windows-lock regression).

**No impl until doyle confirms** — especially: the §2 manifest-refresh mechanism, the §3 own-copy dir location, the §4 new REQ-HAZARD id/shape, and the §6 broker-IPC-vs-seedmap call.


This is an excellent design-gate return — thorough, grounded, and it correctly caught that **ADR-0025 step 3 is not implementable as written**. Before I approve, spot-check the load-bearing claim (it reshapes the wave) + the broker-state claim:


echo "=== livehost.rs:400 — BrainLifecycle moved into thread closure? ==="; sed -n '395,405p' crates/spt-daemon/src/livehost.rs
echo "=== broker.rs:845 translation child + 1112 sessions map ==="; sed -n '843,847p' crates/spt-daemon/src/broker.rs; sed -n '1110,1114p' crates/spt-daemon/src/broker.rs
echo "=== HostedSession struct fields (endpoint/translation) ==="; grep -n "struct HostedSession" crates/spt-daemon/src/broker.rs; sed -n '866,875p' crates/spt-daemon/src/broker.rs
echo "=== sha256_hex exists release.rs:603 ==="; sed -n '600,606p' crates/spt-daemon/src/release.rs
echo "=== psyche_orphan_should_reap + reap_orphan_psyches sites ==="; grep -n "fn psyche_orphan_should_reap\|fn reap_orphan_psyches\|psyche_orphan_should_reap(" crates/spt-daemon/src/livehost.rs

Verify todlando's load-bearing grounding claims


=== livehost.rs:400 — BrainLifecycle moved into thread closure? ===
        }
    };
    let stop = Arc::new(AtomicBool::new(false));
    let handle = {
        let stop = Arc::clone(&stop);
        std::thread::spawn(move || {
            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});
        })
    };
    // Stamp the host instant — the residency-grace anchor a later tick measures
    // the `{id}-psyche` perch's liveness against (REQ-HAZARD-LIVEHOST-NONRESIDENT).
=== broker.rs:845 translation child + 1112 sessions map ===
    /// The live child — shared so `dispatch_input` fires content-free pings while
    /// the worker (which also holds an `Arc`) drives events / faults the binary.
    child: Arc<TranslationChild>,
    /// Raised by the worker on FAULT (commit-deadline miss / binary death): the
    /// session reverts to raw inject and `dispatch_input` stops pinging.
/// recovery anchor a brain crash must not lose, so it lives in the broker, not
/// the brain (Spike #6 constraint 1).
pub struct Broker {
    listener: LocalSocketTransport,
    sessions: Arc<Mutex<HashMap<u64, HostedSession>>>,
=== HostedSession struct fields (endpoint/translation) ===
853:struct HostedSession {
    log: Arc<Mutex<OutputLog>>,
    /// The owning endpoint (the spawn's label; empty = unlabeled). The
    /// inbound dispatcher's gate subject for remote-drive (D9-1).
    endpoint: String,
    /// The translation-binary attachment (Layer C), or `None` for a session with
    /// no `[message-idle-translation-binary]` — then inbound delivery raw-injects.
    translation: Option<Arc<Translation>>,
}

/// One record queued to a session's dedicated PTY input-writer thread — the raw
=== sha256_hex exists release.rs:603 ===

/// Lowercase-hex SHA-256 of `bytes` — the artifact-digest format used in
/// [`ReleaseMetadata::artifact_sha256`].
pub fn sha256_hex(bytes: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    hex_encode(&hasher.finalize())
=== psyche_orphan_should_reap + reap_orphan_psyches sites ===
595:fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {
621:fn reap_orphan_psyches(
654:        if psyche_orphan_should_reap(&psyche_id, &expected, pid) {
1102:        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());
1105:        let spared = !psyche_orphan_should_reap("agent7-psyche", &prog, sibling.id());


All load-bearing claims **verified**: `lifecycle` IS moved into `std::thread::spawn(move ||...)` at livehost.rs:400 (thread-owned, only `stop: AtomicBool` in — IPC-unreachable) → ADR-0025 step 3 genuinely not implementable as written. Broker `sessions` map + `HostedSession{endpoint, translation}` confirmed (broker.rs:853/1112, "lives in the broker, not the brain — a brain crash must not lose"). `sha256_hex` (release.rs:603) + `psyche_orphan_should_reap` (595, brain-start-only at 654) confirmed.

Design is sound and the grounding is real. Writing the gate verdict (binding confirmations → file, not truncatable owl):


Write

# v0.13.2 W3 — DESIGN-GATE VERDICT (doyle → todlando)

**VERDICT: APPROVED to build, with the confirmations + binding refinements below.** I verified the load-bearing grounding against the live tree (livehost.rs:400 BrainLifecycle moved into the `std::thread::spawn(move||run_pulse_loop)` closure — IPC-unreachable, confirmed; broker `sessions` map + `HostedSession{endpoint,translation}` broker.rs:853/1112 confirmed; `sha256_hex` release.rs:603 confirmed; `psyche_orphan_should_reap` 595 / brain-start-only call 654 confirmed). The proposal correctly caught that ADR-0025 step 3 is not implementable as written — good catch; that's what the design-gate was for.

## Confirmations (your proposal stands)

1. **W3a — reuse the broker `sessions` map as the resident registry** (no parallel structure). CONFIRMED. Add `HostedSession{adapter, install_dir}` + the 2 query helpers.
2. **W3c — manifest refresh via a shared `Arc<RwLock<Manifest>>` / `Arc<RwLock<ManifestRuntime>>` handle the brain registers at host into a broker-side per-endpoint map.** CONFIRMED as primary. Binding cautions:
   - **(i) Minimal read-lock scope** — every one of the 4 read sites must `read() → clone what it needs → drop the guard` BEFORE any spawn / blocking call / pulse-body work. A read guard held across `spawn_psyche`/`fire_echo` while an apply wants the write lock is the failure mode. Prove it (a quick comment at each site).
   - **(ii) Handle lifecycle** — the broker-side handle-map entry MUST be inserted at host and **removed on session/host teardown**, and **re-registered on brain restart** (the brain re-hosts; the Arc must not dangle or point at a dead brain's state). No stale handles.
   - **(iii) Fallback** — if the read-lock scoping across the 4 sites proves gnarly, the escape hatch is a brain-owned **reload-on-dirty-signal** (apply sets a per-endpoint `AtomicBool` / sends a signal like the existing `stop` flag; the brain re-reads+rebuilds manifest on its next pulse). `Arc<RwLock>` stays primary because the apply already holds the parsed new manifest (no brain-side re-resolution needed). If you switch, say so in the W3c commit with one line of why.
3. **(c) PRIMARY psyche-from-own-copy at `<perch_dir>/.live-bin/<prog>`** — CONFIRMED. Refinements:
   - **Copy only when absent-or-hash-differs** (reuse `sha256_hex`) so a routine psyche re-spawn/revival is a cheap hash check, not a multi-MB copy every pulse. Copy happens after the reap, when `.live-bin/<prog>` is unlocked.
   - `.live-bin/` is **per-endpoint** and **torn down with the perch**.
   - Your REQ-INSTALL-11 proof is accepted (resolution still finds the shipped binary to source bytes; only the exec path is the identical-content relocated copy; args/`strings/`/`{key}` still resolve against `install_dir`). Add the `[unit]` you proposed (spawned path under `.live-bin/`, bytes == `<install_dir>/<program>`).
   - **Resident translation stays in-install-dir** (supervised → W3d stop-before-swap frees the lock; own-copy unneeded). CONFIRMED — your justification holds.
4. **(a) stop-path / brain-death scoped-reap** — CONFIRMED. Invoke the EXISTING `psyche_orphan_should_reap` (verbatim, fail-safe-decline) at the reconcile stop-side (livehost.rs:280-301) + `confirm_residency_or_unhost` (477), after `stop_host`. Never kill on an unreadable signal.
5. **W3b/(b) — `sha256_hex` CRC, extract-to-temp, atomic per-file write-temp+rename, only-changed** — CONFIRMED (no new dep). Replaces the `remove_dir_all`+blanket-extract os-error-5 path.
6. **W3d — broker IPC `KIND_ADAPTER_APPLY` + `dispatch_adapter_apply` (broker.rs:1306 arm, KIND_KILL pattern), NOT seedmap** — CONFIRMED. None-live → CLI-direct CRC swap; some-live → daemon apply. Brain-parity (BrainLifecycle/broker session/PTY never restart; only resident child cycles) — CONFIRMED.

## New REQ-HAZARD — CONFIRMED, mint it

**`REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP`** — id approved. Shape (use this):
> *Endpoint-stop and brain-death reconcile must reap a brain-less perch's orphan detached Psyche via the cmdline-scoped guard (`psyche_orphan_should_reap`) — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) cannot, because the owning brain is gone (its `psyche_child` handle died with it), and the brain-start scoped-reap (REQ-HAZARD brain-restart-dup) never fires for a perch that is being stopped rather than re-hosted. Preserves fail-safe-decline (pid-alive ∧ basename==psyche-prog ∧ cmdline⊇`<id>-psyche`; any unreadable signal declines — a missed dup is bounded, a wrong-kill is catastrophic).*
- **Stages `["doc","impl","unit","int"]`.** doc = a `docs/KNOWN-HAZARDS.md` entry (rule 4 — every REQ-HAZARD is a KNOWN-HAZARDS row with a test) + the ADR-0025 amendment. unit = scoped-reap-fires-on-stop-of-a-brain-less-orphan (+ a sibling-id is spared). int = W3e step 3.
- Add it to `traceable-reqs.toml` (rule 3: register first) AND `docs/KNOWN-HAZARDS.md`.

## Binding refinements to your plan

- **Partial-failure (§6) — tighten the swap ordering.** Stage ALL changed files as `<file>.new` first, THEN do every rename in one tight final loop (minimize the partial-rename window). On a mid-loop rename failure: best-effort roll back the already-committed renames (you still hold the displaced originals), restart the OLD resident child, report update-failed. The endpoint is **never** left stopped-without-restart, and install_dir is never left half-new with a running new-expecting child. Spell this out in the W3d commit.
- **§9 flag 5 (BINDING):** the translation restart (W3d step 5) MUST re-resolve the binary path from the **refreshed** manifest, not the cached `SpawnReq.translation_binary`, so an update that renames/moves the translation binary is picked up.
- **ADR-0025 amendment** — write it: add the **third axis** to the resident/ephemeral split (*ephemeral-but-install-dir-locking → run from a per-endpoint own copy*); correct the two errors (psyche-is-in-daemon; re-clone-into-running-BrainLifecycle); document broker-as-registry + manifest-refresh-via-shared-handle + the stop-path reap. Mark ADR-0025 status accepted (was proposed).

## Build order — APPROVED, I gate incrementally

`W3a` + `W3b` (low-risk helpers — push together, I gate as a batch) → **`W3c`** (the `Arc<RwLock>` + handle registration — I gate this one carefully, it touches the hardened brain) → **(a)+(c)** (reap + own-copy — gate together) → **`W3d`** (IPC + apply + partial-failure — gate carefully) → **`W3e`** (NO-mocks int + Windows-lock regression — the ship-blocker, I gate against perri's exact repro). Reviewable sub-commits, co-author trailer `Co-authored by: todlando`, push each (PR #29 CI validates both runners). Activate `REQ-ADAPTER-LIVE-UPDATE` → `["doc","impl","unit","int"]` and mint the new REQ-HAZARD at the wave.

Proceed. Ping me at each gate point in the build order (or batch W3a+W3b). I'll also loop perri to validate her exact repro once W3e lands.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\W3-GATE-VERDICT.md (file state is current in your context — no need to Read it back)

# v0.13.2 W3 — DESIGN-GATE VERDICT (doyle → todlando)

**VERDICT: APPROVED to build, with the confirmations + binding refinements below.** I verified the load-bearing grounding against the live tree (livehost.rs:400 BrainLifecycle moved into the `std::thread::spawn(move||run_pulse_loop)` closure — IPC-unreachable, confirmed; broker `sessions` map + `HostedSession{endpoint,translation}` broker.rs:853/1112 confirmed; `sha256_hex` release.rs:603 confirmed; `psyche_orphan_should_reap` 595 / brain-start-only call 654 confirmed). The proposal correctly caught that ADR-0025 step 3 is not implementable as written — good catch; that's what the design-gate was for.

## Confirmations (your proposal stands)

1. **W3a — reuse the broker `sessions` map as the resident registry** (no parallel structure). CONFIRMED. Add `HostedSession{adapter, install_dir}` + the 2 query helpers.
2. **W3c — manifest refresh via a shared `Arc<RwLock<Manifest>>` / `Arc<RwLock<ManifestRuntime>>` handle the brain registers at host into a broker-side per-endpoint map.** CONFIRMED as primary. Binding cautions:
   - **(i) Minimal read-lock scope** — every one of the 4 read sites must `read() → clone what it needs → drop the guard` BEFORE any spawn / blocking call / pulse-body work. A read guard held across `spawn_psyche`/`fire_echo` while an apply wants the write lock is the failure mode. Prove it (a quick comment at each site).
   - **(ii) Handle lifecycle** — the broker-side handle-map entry MUST be inserted at host and **removed on session/host teardown**, and **re-registered on brain restart** (the brain re-hosts; the Arc must not dangle or point at a dead brain's state). No stale handles.
   - **(iii) Fallback** — if the read-lock scoping across the 4 sites proves gnarly, the escape hatch is a brain-owned **reload-on-dirty-signal** (apply sets a per-endpoint `AtomicBool` / sends a signal like the existing `stop` flag; the brain re-reads+rebuilds manifest on its next pulse). `Arc<RwLock>` stays primary because the apply already holds the parsed new manifest (no brain-side re-resolution needed). If you switch, say so in the W3c commit with one line of why.
3. **(c) PRIMARY psyche-from-own-copy at `<perch_dir>/.live-bin/<prog>`** — CONFIRMED. Refinements:
   - **Copy only when absent-or-hash-differs** (reuse `sha256_hex`) so a routine psyche re-spawn/revival is a cheap hash check, not a multi-MB copy every pulse. Copy happens after the reap, when `.live-bin/<prog>` is unlocked.
   - `.live-bin/` is **per-endpoint** and **torn down with the perch**.
   - Your REQ-INSTALL-11 proof is accepted (resolution still finds the shipped binary to source bytes; only the exec path is the identical-content relocated copy; args/`strings/`/`{key}` still resolve against `install_dir`). Add the `[unit]` you proposed (spawned path under `.live-bin/`, bytes == `<install_dir>/<program>`).
   - **Resident translation stays in-install-dir** (supervised → W3d stop-before-swap frees the lock; own-copy unneeded). CONFIRMED — your justification holds.
4. **(a) stop-path / brain-death scoped-reap** — CONFIRMED. Invoke the EXISTING `psyche_orphan_should_reap` (verbatim, fail-safe-decline) at the reconcile stop-side (livehost.rs:280-301) + `confirm_residency_or_unhost` (477), after `stop_host`. Never kill on an unreadable signal.
5. **W3b/(b) — `sha256_hex` CRC, extract-to-temp, atomic per-file write-temp+rename, only-changed** — CONFIRMED (no new dep). Replaces the `remove_dir_all`+blanket-extract os-error-5 path.
6. **W3d — broker IPC `KIND_ADAPTER_APPLY` + `dispatch_adapter_apply` (broker.rs:1306 arm, KIND_KILL pattern), NOT seedmap** — CONFIRMED. None-live → CLI-direct CRC swap; some-live → daemon apply. Brain-parity (BrainLifecycle/broker session/PTY never restart; only resident child cycles) — CONFIRMED.

## New REQ-HAZARD — CONFIRMED, mint it

**`REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP`** — id approved. Shape (use this):
> *Endpoint-stop and brain-death reconcile must reap a brain-less perch's orphan detached Psyche via the cmdline-scoped guard (`psyche_orphan_should_reap`) — the handle-reap (`stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) cannot, because the owning brain is gone (its `psyche_child` handle died with it), and the brain-start scoped-reap (REQ-HAZARD brain-restart-dup) never fires for a perch that is being stopped rather than re-hosted. Preserves fail-safe-decline (pid-alive ∧ basename==psyche-prog ∧ cmdline⊇`<id>-psyche`; any unreadable signal declines — a missed dup is bounded, a wrong-kill is catastrophic).*
- **Stages `["doc","impl","unit","int"]`.** doc = a `docs/KNOWN-HAZARDS.md` entry (rule 4 — every REQ-HAZARD is a KNOWN-HAZARDS row with a test) + the ADR-0025 amendment. unit = scoped-reap-fires-on-stop-of-a-brain-less-orphan (+ a sibling-id is spared). int = W3e step 3.
- Add it to `traceable-reqs.toml` (rule 3: register first) AND `docs/KNOWN-HAZARDS.md`.

## Binding refinements to your plan

- **Partial-failure (§6) — tighten the swap ordering.** Stage ALL changed files as `<file>.new` first, THEN do every rename in one tight final loop (minimize the partial-rename window). On a mid-loop rename failure: best-effort roll back the already-committed renames (you still hold the displaced originals), restart the OLD resident child, report update-failed. The endpoint is **never** left stopped-without-restart, and install_dir is never left half-new with a running new-expecting child. Spell this out in the W3d commit.
- **§9 flag 5 (BINDING):** the translation restart (W3d step 5) MUST re-resolve the binary path from the **refreshed** manifest, not the cached `SpawnReq.translation_binary`, so an update that renames/moves the translation binary is picked up.
- **ADR-0025 amendment** — write it: add the **third axis** to the resident/ephemeral split (*ephemeral-but-install-dir-locking → run from a per-endpoint own copy*); correct the two errors (psyche-is-in-daemon; re-clone-into-running-BrainLifecycle); document broker-as-registry + manifest-refresh-via-shared-handle + the stop-path reap. Mark ADR-0025 status accepted (was proposed).

## Build order — APPROVED, I gate incrementally

`W3a` + `W3b` (low-risk helpers — push together, I gate as a batch) → **`W3c`** (the `Arc<RwLock>` + handle registration — I gate this one carefully, it touches the hardened brain) → **(a)+(c)** (reap + own-copy — gate together) → **`W3d`** (IPC + apply + partial-failure — gate carefully) → **`W3e`** (NO-mocks int + Windows-lock regression — the ship-blocker, I gate against perri's exact repro). Reviewable sub-commits, co-author trailer `Co-authored by: todlando`, push each (PR #29 CI validates both runners). Activate `REQ-ADAPTER-LIVE-UPDATE` → `["doc","impl","unit","int"]` and mint the new REQ-HAZARD at the wave.

Proceed. Ping me at each gate point in the build order (or batch W3a+W3b). I'll also loop perri to validate her exact repro once W3e lands.


W2 — gh transport for private repos

W4 — update-message

Release — cut v0.13.2 (deployah role)


Commit the verdict + send todlando the go-ahead: