W2 — gh transport for private repos

W4 — update-message

Release — cut v0.13.2 (deployah role)

v0.13.3 fast-follow — multi-subnet endpoint-run bind LATENT GAP (perri)

LATENT GAP (perri, 2026-06-22, operator-hit) — NOT a version regression (perri pinned: 0.11/0.12/0.12.1/0.13.1 all HOME_REFUSED; the no-guess multi-subnet home policy is established ≥0.11.0). The spt-hosted bringup NEVER supported a multi-subnet home — only worked while the node was single-subnet; the BIGNET 2nd-subnet crossing exposed it (operator misread version-bump+2nd-subnet coincidence as a regression). `spt endpoint run` SessionStart bind hook `spt api bind "$SPT_ENDPOINT_ID" --set-session-id "$sid"` (no --subnet) → HOME_REFUSED (cmd_bind api/startup.rs:432 → establish_perch; None subnet on multi-subnet refuses) → hook swallows (|| true) → silent no-perch → ENDPOINT_RUN_ONLINE_TIMEOUT 25s. Single-subnet unaffected (auto-homes). F-013 class.

FIX (harness-agnostic, being grilled with operator 2026-06-22): (1) endpoint run resolves home — new --subnet flag → else ??? → else ERROR EARLY (clear msg, not silent 25s); (2) broker injects $SPT_ENDPOINT_SUBNET; (3) cmd_bind env-fallback: --subnet → $SPT_ENDPOINT_SUBNET → single-auto/multi-refuse, hooks unchanged. Mirrors F-013 thread-through. NO node-default-home concept exists (home is per-endpoint, sole-subnet-auto / multi-must-specify).

DISPOSITION: v0.13.3 EXPEDITE right after v0.13.2 (operator DECLINED the temp stopgap, waiting for real fix → staying multi-subnet-blocked by choice). Mint REQ (rule 3) + dispatch todlando after v0.13.2 cuts. perri committing repro script + findings entry (regression test MUST run on multi-subnet node — single-subnet hides the gap). Grill in progress to finalize the home-resolution policy.


Read the (a)+(c) impl (reap wrappers + psyche own-copy):


Bash

git diff 97ff869..d6d7816 -- crates/spt-daemon/src/lifecycle.rs | grep -v "^+.*//\|^-.*//" | sed -n '1,120p'

Read lifecycle (a)+(c) impl (copy_if_changed/psyche_spawn_runtime)


diff --git a/crates/spt-daemon/src/lifecycle.rs b/crates/spt-daemon/src/lifecycle.rs
index 85af0f1..bbbac52 100644
--- a/crates/spt-daemon/src/lifecycle.rs
+++ b/crates/spt-daemon/src/lifecycle.rs
@@ -120,6 +120,27 @@ fn build_runtime(manifest: &Manifest, install_dir: Option<&Path>) -> ManifestRun
     }
 }

+fn copy_if_changed(src: &Path, dest: &Path) -> std::io::Result<()> {
+    let differs = match std::fs::read(dest) {
+        Ok(have) => {
+            crate::release::sha256_hex(&have) != crate::release::sha256_hex(&std::fs::read(src)?)
+        }
+    };
+    if differs {
+        if let Some(parent) = dest.parent() {
+            std::fs::create_dir_all(parent)?;
+        }
+        std::fs::copy(src, dest)?;
+    }
+    Ok(())
+}
+
 /// The daemon-hosted LiveAgent lifecycle: the runtime + watched drop dirs built
 /// from a live-capable manifest, paced by a config-driven [`DaemonConfig`].
 pub struct BrainLifecycle {
@@ -287,6 +308,57 @@ impl BrainLifecycle {
         }
     }

+    fn psyche_spawn_runtime(&self) -> ManifestRuntime {
+        let manifest = self.cell.manifest_snapshot();
+        let Some(install_dir) = self.install_dir.as_ref() else {
+            return self.cell.runtime_snapshot();
+        };
+        let Some(prog_token) = manifest
+            .session
+            .psyche_init
+            .as_ref()
+            .and_then(|role| role.command.split_whitespace().next())
+        else {
+            return self.cell.runtime_snapshot();
+        };
+        let src = spt_runtime::runtime::resolve_program_in_dir(prog_token, install_dir);
+        let src_path = Path::new(&src);
+        let Some(file_name) = src_path.file_name().filter(|_| src_path.is_file()) else {
+            return self.cell.runtime_snapshot();
+        };
+        let live_bin = perch::resolve_perch_path(&self.id, ParentHint::Infer).join(".live-bin");
+        let dest = live_bin.join(file_name);
+        if let Err(e) = copy_if_changed(src_path, &dest) {
+            eprintln!(
+                "PSYCHE_OWNCOPY_FALLBACK:{}: {e} (spawning from install dir)",
+                self.id
+            );
+            return self.cell.runtime_snapshot();
+        }
+        ManifestRuntime::with_install_dir(manifest, &live_bin)
+    }
+
     /// Spawn the Psyche companion (T2). `event` is the incoming envelope when the
     /// spawn is event-driven; `None` at cold start.
     ///
@@ -301,9 +373,10 @@ impl BrainLifecycle {
         session_id: Option<&str>,
         event: Option<&str>,
     ) -> Result<PsycheHandle, String> {
-        let runtime = self.cell.runtime_snapshot();
+        let runtime = self.psyche_spawn_runtime();
         let handle = spawn_psyche(
             &runtime,
             &self.id,
@@ -328,8 +401,8 @@ impl BrainLifecycle {
         session_id: Option<&str>,
         event: Option<&str>,
     ) -> Result<(PsycheHandle, std::process::Child), String> {
-        let runtime = self.cell.runtime_snapshot();
+        let runtime = self.psyche_spawn_runtime();
         let (handle, child) = spawn_psyche_owned(
             &runtime,
             &self.id,
@@ -1480,4 +1553,144 @@ mod tests {
             );
         });
     }
+
+
+    fn psyche_prog_token() -> String {
+        NOOP_CMD.split_whitespace().next().unwrap().to_string()
+    }
+
+    #[test]
+    fn copy_if_changed_copies_then_skips() {
+        let tmp = tempfile::tempdir().unwrap();
+        let src = tmp.path().join("src.bin");
+        let dest = tmp.path().join("nested").join("dest.bin");
+
+        std::fs::write(&src, b"alpha-content").unwrap();
+        copy_if_changed(&src, &dest).unwrap();
+        assert!(dest.is_file(), "copy_if_changed creates the dest (and its parent)");
+        assert_eq!(
+            std::fs::read(&dest).unwrap(),
+            b"alpha-content",
+            "the dest gets the src bytes"
+        );
+
+        std::fs::write(&src, b"steady-content").unwrap();
+        std::fs::write(&dest, b"steady-content").unwrap();
+        let before = std::fs::metadata(&dest).unwrap().modified().unwrap();
+        copy_if_changed(&src, &dest).unwrap();
+        let after = std::fs::metadata(&dest).unwrap().modified().unwrap();
+        assert_eq!(
+            before, after,
+            "identical content ⇒ no re-copy (dest mtime unchanged)"
+        );
+        assert_eq!(
+            std::fs::read(&dest).unwrap(),
+            b"steady-content",
+            "the unchanged dest keeps its content"
+        );


Two gate concerns to verify before passing (a)+(c):
1. `copy_if_changed`'s `match` shows only an `Ok` arm in my comment-stripped view — need to confirm the `Err` arm (absent-dest → copy) exists (else non-exhaustive; my grep may have eaten a trailing-comment line).
2. `psyche_spawn_runtime` pins `ManifestRuntime::with_install_dir(manifest, &live_bin)` — if the runtime uses `install_dir` for anything beyond program resolution (e.g. `strings/`, sibling paths), pinning `.live-bin` (which holds only the copied binary) would break it. todlando claims args/strings still resolve against the real install_dir — verify.


echo "=== copy_if_changed full (with comments) ==="; sed -n '123,138p' crates/spt-daemon/src/lifecycle.rs
echo "=== what does ManifestRuntime use install_dir for? ==="; grep -n "install_dir" crates/spt-runtime/src/runtime.rs | head -25

Verify copy_if_changed Err arm + install_dir usage scope


=== copy_if_changed full (with comments) ===
/// Copy `src` → `dest` only when `dest` is absent or its content hash differs
/// (reuse [`crate::release::sha256_hex`]) — so a routine psyche re-spawn is a
/// cheap hash compare, not a multi-MB copy each time. Creates `dest`'s parent.
/// (W3 (c) psyche own-copy.)
// [impl->REQ-ADAPTER-LIVE-UPDATE]
fn copy_if_changed(src: &Path, dest: &Path) -> std::io::Result<()> {
    let differs = match std::fs::read(dest) {
        Ok(have) => {
            crate::release::sha256_hex(&have) != crate::release::sha256_hex(&std::fs::read(src)?)
        }
        Err(_) => true, // absent (or unreadable) ⇒ (re)copy
    };
    if differs {
        if let Some(parent) = dest.parent() {
            std::fs::create_dir_all(parent)?;
        }
=== what does ManifestRuntime use install_dir for? ===
287:    install_dir: Option<PathBuf>,
294:            install_dir: None,
301:    pub fn with_install_dir(manifest: Manifest, install_dir: impl Into<PathBuf>) -> Self {
304:            install_dir: Some(install_dir.into()),
338:        let program = match &self.install_dir {
521:/// Returns `<install_dir>/<program>` (absolute) when that file exists — on
527:pub fn resolve_program_in_dir(program: &str, install_dir: &Path) -> String {
528:    let direct = install_dir.join(program);
535:            let exe = install_dir.join(format!("{program}.exe"));
544:/// As [`run_bounded_command`], but when `install_dir` is `Some`, the template's
556:    install_dir: Option<&Path>,
565:    let program = match install_dir {
805:    fn resolve_program_prefers_install_dir() {


Both verified clean:
1. `copy_if_changed` **Err arm exists** (`Err(_) => true, // absent ⇒ (re)copy`) — my comment-strip grep ate it. Compiles, correct.
2. `ManifestRuntime` uses `install_dir` **solely for program resolution** (runtime.rs:338 → `resolve_program_in_dir`); `strings/`/`{key}` are manifest-string fills, install_dir-independent. So pinning `.live-bin` changes *only* which binary the psyche execs (→ the hash-identical copy) — REQ-INSTALL-11 holds, nothing else shifts. The (c) own-copy is correct.

Verify the (a) reap wiring (verbatim guard reuse + 2 call sites) + run gates:


echo "=== reap fns + verbatim guard reuse + call sites ==="; grep -n "fn reap_orphan_psyche_for\|fn reap_stopped_endpoint_orphan_psyche\|psyche_orphan_should_reap\|reap_stopped_endpoint_orphan_psyche(" crates/spt-daemon/src/livehost.rs | head
echo "=== clippy ==="; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -2; echo "CLIPPY=${PIPESTATUS[0]}"
echo "=== (a)+(c) units ==="; timeout 150 cargo test -p spt-daemon --lib "copy_if_changed" 2>&1 | grep -E "test result"; timeout 150 cargo test -p spt-daemon --lib "psyche" 2>&1 | grep -E "test lifecycle|test livehost|test result" | tail -12
echo "=== traceable ==="; "C:/Users/decid/AppData/Local/Programs/traceable-reqs/traceable-reqs" check >/tmp/tac.txt 2>&1; echo "EXIT=$?"; grep -E "REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP|REQ-INSTALL-11 " /tmp/tac.txt | head

Verify reap wiring + clippy + (a)(c) units + traceable


=== reap fns + verbatim guard reuse + call sites ===
296:            reap_stopped_endpoint_orphan_psyche(&id);
485:            reap_stopped_endpoint_orphan_psyche(id);
603:fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {
625:/// pre-existing `{id}-psyche` orphan IFF [`psyche_orphan_should_reap`] (the
640:/// cmdline-scoped [`psyche_orphan_should_reap`] (fail-safe-decline: pid alive AND
649:fn reap_orphan_psyche_for(
682:    if psyche_orphan_should_reap(&psyche_id, &expected, pid) {
702:fn reap_stopped_endpoint_orphan_psyche(id: &str) {
1146:        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());
1149:        let spared = !psyche_orphan_should_reap("agent7-psyche", &prog, sibling.id());
=== clippy ===
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.80s
CLIPPY=0
=== (a)+(c) units ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 371 filtered out; finished in 0.02s
test lifecycle::tests::daemon_hosted_psyche_liveness_is_status_not_pid ... ok
test lifecycle::tests::psyche_spawn_runtime_falls_back_without_install_dir ... ok
test lifecycle::tests::psyche_spawn_runtime_uses_live_bin_own_copy ... ok
test livehost::tests::stop_host_reaps_the_detached_psyche_process ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 368 filtered out; finished in 0.20s
=== traceable ===
EXIT=0
  [OK] REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-INSTALL-11  required: [doc, impl, unit]  stages: +doc +impl +unit -int


**(a)+(c) = GATE-PASS** @d6d7816. Reap wired at both stop-path sites (296, 485) reusing `psyche_orphan_should_reap` verbatim (fail-safe-decline); own-copy correct (install_dir used solely for program resolution, so `.live-bin` pin only relocates the hash-identical psyche binary). clippy 0, units pass, traceable 0 (289/289, `REQ-HAZARD-STOP-PATH` + `REQ-INSTALL-11` both `+doc+impl+unit`). The perri psyche-lock fix is now implemented.

Report + greenlight W3d, and ground the skeleton-perch mechanism for grill Q3 (parallel):


"$OWL" send todlando doyle <<'EOF'
W3 (a)+(c) GATE-PASS @d6d7816. Verified myself:
- (c) own-copy: copy_if_changed Err-arm correct (absent→copy); ManifestRuntime uses install_dir SOLELY for program resolution (runtime.rs:338 resolve_program_in_dir) — so pinning with_install_dir(.live-bin) relocates ONLY the psyche binary exec (a hash-identical copy of the shipped binary), strings/{key} are manifest-string fills unaffected. REQ-INSTALL-11 holds exactly. Graceful fallback to install-dir spawn on any gap. Both spawn sites use psyche_spawn_runtime.
- (a) reap: reap_orphan_psyche_for reuses psyche_orphan_should_reap VERBATIM (682, fail-safe-decline), wired after stop_host at reconcile stop-side (296) + confirm_residency_or_unhost (485). Belt-and-suspenders w/ the handle-reap. REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP minted (traceable + KNOWN-HAZARDS 7.22, [doc,impl,unit], int=W3e).
- clippy EXIT 0; (a)+(c) units pass; traceable EXIT 0 (289/289, REQ-HAZARD-STOP-PATH + REQ-INSTALL-11 both +doc+impl+unit). Clean scope-audit (the W3c lesson held).

GREENLIGHT W3d — the both-process apply. Your ripple is confirmed coherent: CLI stages+verifies; none-live→CLI-direct apply_crc_swap; some-live→KIND_ADAPTER_APPLY to the broker→dispatch_adapter_apply per endpoint (stop translation terminate()+reap psyche → CRC swap → restart translation re-resolving path from the NEW on-disk manifest); brain self-heals manifest via W3c reload-on-dirty (apply does NOT wait; CLI reports success on broker-side swap+restart); partial-failure rollback+restart-OLD; psyche own-copy means the swap never hits a locked psyche binary. Build it; ping me — I gate W3d carefully (cross-process apply), then W3e (the no-mocks keystone vs perri's 4-step repro). Strong work.
EOF
echo "---sent $?---"

Report (a)+(c) pass + greenlight W3d


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


echo "=== does endpoint run pre-create a perch before harness bind? ==="; grep -n "fn cmd_endpoint_run\|skeleton\|resolve_perch_path\|write_info\|create_dir_all.*perch\|launch_harness" crates/spt/src/cli.rs | head -20
echo "=== is stdin/tty interactivity detected anywhere (for the interactive-vs-not branch)? ==="; grep -rn "is_terminal\|IsTerminal\|atty\|stdin().is_tty\|isatty" crates/spt/src 2>/dev/null | head

Ground skeleton-perch feasibility + TTY detection


=== does endpoint run pre-create a perch before harness bind? ===
1301:pub(crate) fn cmd_endpoint_run(
1374:                        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
1384:    match spt_daemon::harnesshost::launch_harness_brokered_in(
1427:    // launch_harness_brokered_in returns once the harness PROCESS is spawned, but the
1463:        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
1529:    let src_perch = perch::resolve_perch_path(src, ParentHint::Infer);
1534:    let new_perch = perch::resolve_perch_path(new_id, ParentHint::Infer);
1571:    if let Err(e) = std::fs::create_dir_all(&new_perch) {
1585:    if let Err(e) = spt_store::info::write_info(&new_perch, &rec) {
1636:            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
1659:            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
1784:        let info = spt_store::info::read_info(&perch::resolve_perch_path(&self_id, ParentHint::Infer));
2298:    let perch_path = perch::resolve_perch_path(issuer, ParentHint::Infer);
2305:    spt_store::info::read_info(&perch::resolve_perch_path(&owner, ParentHint::Infer))?.home_subnet
3122:            let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
3172:    spt_store::info::read_info(&perch::resolve_perch_path(target, ParentHint::Infer))
3502:    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
7766:    let perch = perch::resolve_perch_path(id, ParentHint::Infer);
7783:            perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
8035:        let perch = perch::resolve_perch_path(id, ParentHint::Infer);
=== is stdin/tty interactivity detected anywhere (for the interactive-vs-not branch)? ===
crates/spt/src/cli.rs:1005:    use std::io::IsTerminal;
crates/spt/src/cli.rs:1006:    match decide_bare(std::io::stdin().is_terminal(), std::io::stdout().is_terminal()) {
crates/spt/src/cli.rs:3335:    use std::io::IsTerminal;
crates/spt/src/cli.rs:3337:    let auto = std::env::var_os("NO_COLOR").is_none() && std::io::stderr().is_terminal();
crates/spt/src/cli.rs:3955:    use std::io::IsTerminal;
crates/spt/src/cli.rs:3956:    let interactive = std::io::stdin().is_terminal() && std::io::stderr().is_terminal();
crates/spt/src/cli.rs:5211:    use std::io::IsTerminal;
crates/spt/src/cli.rs:5212:    if !std::io::stdin().is_terminal() {
crates/spt/src/cli.rs:5941:                    use std::io::IsTerminal;
crates/spt/src/cli.rs:5942:                    if std::io::stdin().is_terminal() {


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/12" id="d3f51308" type="msg" from="todlando">W3d split into 2 reviewable sub-commits (the broker-restart refactor in the v0.13.0-hardened broker is intricate enough to gate on its own — your &quot;careful gate&quot; preference). W3d-i DONE @b62476e (pushed). Ready for your gate on i; design for ii below.

W3d-i (CLI-side, contained, low-risk): cmd_adapt</EVENT-PART>
<EVENT-PART seq="2/12" id="d3f51308">er_update's apply now = apply_release_crc_swap (extract staged -&gt; temp tree -&gt; spt_daemon::crc_swap plan+apply into the install dir), replacing remove_dir_all+blanket-extract. Only-changed files swapped (running unchanged binaries untouched), never-strand rollback, ADDITIVE (archive-absent files left in place — a vanished-from-manifest</EVENT-PART>
<EVENT-PART seq="3/12" id="d3f51308"> running binary is never yanked). With (c) own-copy, the ONLY install-dir locker is now a resident translation binary, so: no-translation / no-live-endpoint adapters update LIVE cleanly today; locked cases fail SAFELY (rollback, install intact) instead of destructively — pending ii. 1 int-style unit (real .spt over a populated dest: changed </EVENT-PART>
<EVENT-PART seq="4/12" id="d3f51308">swapped, local file retained, no temp residue, idempotent). clippy --workspace --all-targets -D warnings clean; traceable exit 0; drift OK. Scope-audited (git diff --stat: only cli.rs, impl = apply_release_crc_swap + the call-site; rest test mod).

W3d-ii PLAN (the keystone — broker daemon-coordinated apply for a LIVE locked translatio</EVENT-PART>
<EVENT-PART seq="5/12" id="d3f51308">n binary; your careful gate):
- DETECT (CLI): roster::enumerate() x info.adapter x is_perch_alive -&gt; live endpoints on adapter X. If none -&gt; the W3d-i CLI-direct crc swap (done). If some -&gt; route to the broker.
- IPC: new KIND_ADAPTER_APPLY (msg.rs) + AdapterApplyReq{adapter, staging_dir, install_dir}; CLI extracts staged -&gt; </EVENT-PART>
<EVENT-PART seq="6/12" id="d3f51308">temp staging, sends the staging PATH to the broker (same machine), then registry::register after. Client = Brain::cold_start(broker_socket_name) + a new pub Brain::adapter_apply() (send + read reply, KIND_APPLIED/KIND_ERROR). Broker arm at handle_conn:1364 -&gt; dispatch_adapter_apply (KIND_KILL pattern).
- BROKER dispatch_adapter_apply: fo</EVENT-PART>
<EVENT-PART seq="7/12" id="d3f51308">r each endpoint running the adapter (endpoints_running_adapter + resident_translations_for_endpoint, the W3a helpers): (1) clone the Arc&lt;Translation&gt;, drop the sessions lock, TranslationChild::terminate() (bounded 500ms) -&gt; frees the lock; (2) crc_swap::plan+apply(staging_dir, install_dir) ONCE; (3) re-read install_dir/manifest.toml -</EVENT-PART>
<EVENT-PART seq="8/12" id="d3f51308">&gt; message_idle_translation_binary.path (the NEW path) -&gt; rebuild Translation via a FACTORED build_translation (extracted from dispatch_spawn:1604-1638: fresh cmd/event channels + TranslationChi
...(truncated)</event>
</task-notification>


Grounding confirms: `endpoint run` already resolves the perch path at cli.rs:1374 (pre-spawn), and TTY detection via `std::io::stdin().is_terminal()` is an established pattern (the interactive/non-interactive branch is trivially implementable).

Resuming the grill. Your "skeleton perch" idea actually lets us **drop my original 3-part env fix entirely** — which is the next decision. Question 3:


AskUserQuestion

Question: How should the chosen home reach the perch the harness binds? You described assigning it to a 'skeleton perch.' That's cleaner than my original env plan — confirming the mechanism, since it changes how much we build.

Choices:
endpoint run pre-writes the perch info.json with home_subnet set (skeleton: home assigned, no session yet). The SessionStart `bind` hits establish_perch's prior-branch → INHERITS the home (existing immutable-inherit code) → no HOME_REFUSED, no --subnet in the hook, no env plumbing, no cmd_bind change. Bonus: the skeleton is a natural attach target for your attach-ASAP goal. Wrinkles to resolve next: cleanup if the harness never binds; list/picker visibility of a pre-bind skeleton.
endpoint run (home resolved=SPT_DEV)
  → write perch info.json {home_subnet:SPT_DEV,
     state:live_agent, session:""}   # skeleton
  → spawn harness
     → hook: spt api bind $ID --set-session-id S
        → establish_perch sees prior(skeleton)
        → inherits home SPT_DEV  # no refuse, no flag
NET: ~0 change to bind/hook; reuses immutable-inherit
broker injects $SPT_ENDPOINT_SUBNET into the harness env; cmd_bind gains an env-fallback (--subnet → $SPT_ENDPOINT_SUBNET → single-auto/multi-refuse). Hooks unchanged too, but adds env plumbing through endpoint run→broker→SpawnReq + a cmd_bind branch, and gives no pre-bind perch to attach to.
endpoint run (home=SPT_DEV)
  → broker SpawnReq{env:{SPT_ENDPOINT_SUBNET:SPT_DEV}}
  → spawn harness
     → hook: spt api bind $ID --set-session-id S
        → cmd_bind reads $SPT_ENDPOINT_SUBNET fallback
        → home SPT_DEV
NET: env plumbing + cmd_bind branch; no skeleton perch


Your questions have been answered: "How should the chosen home reach the perch the harness binds? You described assigning it to a 'skeleton perch.' That's cleaner than my original env plan — confirming the mechanism, since it changes how much we build."=(no option selected) notes: i'm pretty sure skeleton perch is not a new concept. it already exists when `spt api bind` is called. check CONTEXT.md.. You can now continue with these answers in mind.

Question: How should the chosen home reach the perch the harness binds? You described assigning it to a 'skeleton perch.' That's cleaner than my original env plan — confirming the mechanism, since it changes how much we build.

Choices:
endpoint run pre-writes the perch info.json with home_subnet set (skeleton: home assigned, no session yet). The SessionStart `bind` hits establish_perch's prior-branch → INHERITS the home (existing immutable-inherit code) → no HOME_REFUSED, no --subnet in the hook, no env plumbing, no cmd_bind change. Bonus: the skeleton is a natural attach target for your attach-ASAP goal. Wrinkles to resolve next: cleanup if the harness never binds; list/picker visibility of a pre-bind skeleton.
endpoint run (home resolved=SPT_DEV)
  → write perch info.json {home_subnet:SPT_DEV,
     state:live_agent, session:""}   # skeleton
  → spawn harness
     → hook: spt api bind $ID --set-session-id S
        → establish_perch sees prior(skeleton)
        → inherits home SPT_DEV  # no refuse, no flag
NET: ~0 change to bind/hook; reuses immutable-inherit
broker injects $SPT_ENDPOINT_SUBNET into the harness env; cmd_bind gains an env-fallback (--subnet → $SPT_ENDPOINT_SUBNET → single-auto/multi-refuse). Hooks unchanged too, but adds env plumbing through endpoint run→broker→SpawnReq + a cmd_bind branch, and gives no pre-bind perch to attach to.
endpoint run (home=SPT_DEV)
  → broker SpawnReq{env:{SPT_ENDPOINT_SUBNET:SPT_DEV}}
  → spawn harness
     → hook: spt api bind $ID --set-session-id S
        → cmd_bind reads $SPT_ENDPOINT_SUBNET fallback
        → home SPT_DEV
NET: env plumbing + cmd_bind branch; no skeleton perch


Prompt:
<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/12" id="d3f51308" type="msg" from="todlando">W3d split into 2 reviewable sub-commits (the broker-restart refactor in the v0.13.0-hardened broker is intricate enough to gate on its own — your &quot;careful gate&quot; preference). W3d-i DONE @b62476e (pushed). Ready for your gate on i; design for ii below.

W3d-i (CLI-side, contained, low-risk): cmd_adapt</EVENT-PART>
<EVENT-PART seq="2/12" id="d3f51308">er_update's apply now = apply_release_crc_swap (extract staged -&gt; temp tree -&gt; spt_daemon::crc_swap plan+apply into the install dir), replacing remove_dir_all+blanket-extract. Only-changed files swapped (running unchanged binaries untouched), never-strand rollback, ADDITIVE (archive-absent files left in place — a vanished-from-manifest</EVENT-PART>
<EVENT-PART seq="3/12" id="d3f51308"> running binary is never yanked). With (c) own-copy, the ONLY install-dir locker is now a resident translation binary, so: no-translation / no-live-endpoint adapters update LIVE cleanly today; locked cases fail SAFELY (rollback, install intact) instead of destructively — pending ii. 1 int-style unit (real .spt over a populated dest: changed </EVENT-PART>
<EVENT-PART seq="4/12" id="d3f51308">swapped, local file retained, no temp residue, idempotent). clippy --workspace --all-targets -D warnings clean; traceable exit 0; drift OK. Scope-audited (git diff --stat: only cli.rs, impl = apply_release_crc_swap + the call-site; rest test mod).

W3d-ii PLAN (the keystone — broker daemon-coordinated apply for a LIVE locked translatio</EVENT-PART>
<EVENT-PART seq="5/12" id="d3f51308">n binary; your careful gate):
- DETECT (CLI): roster::enumerate() x info.adapter x is_perch_alive -&gt; live endpoints on adapter X. If none -&gt; the W3d-i CLI-direct crc swap (done). If some -&gt; route to the broker.
- IPC: new KIND_ADAPTER_APPLY (msg.rs) + AdapterApplyReq{adapter, staging_dir, install_dir}; CLI extracts staged -&gt; </EVENT-PART>
<EVENT-PART seq="6/12" id="d3f51308">temp staging, sends the staging PATH to the broker (same machine), then registry::register after. Client = Brain::cold_start(broker_socket_name) + a new pub Brain::adapter_apply() (send + read reply, KIND_APPLIED/KIND_ERROR). Broker arm at handle_conn:1364 -&gt; dispatch_adapter_apply (KIND_KILL pattern).
- BROKER dispatch_adapter_apply: fo</EVENT-PART>
<EVENT-PART seq="7/12" id="d3f51308">r each endpoint running the adapter (endpoints_running_adapter + resident_translations_for_endpoint, the W3a helpers): (1) clone the Arc&lt;Translation&gt;, drop the sessions lock, TranslationChild::terminate() (bounded 500ms) -&gt; frees the lock; (2) crc_swap::plan+apply(staging_dir, install_dir) ONCE; (3) re-read install_dir/manifest.toml -</EVENT-PART>
<EVENT-PART seq="8/12" id="d3f51308">&gt; message_idle_translation_binary.path (the NEW path) -&gt; rebuild Translation via a FACTORED build_translation (extracted from dispatch_spawn:1604-1638: fresh cmd/event channels + TranslationChi
...(truncated)</event>
</task-notification>


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.

Tracking v0.13.3 multi-subnet bind regression


The user corrects my framing — the skeleton perch already exists as a concept. Let me verify in CONTEXT.md:


66:The stable set of `spt.exe <subcommand>` entry points that harnesses bind their own hook systems to. When the harness's runtime emits an event (subagent started, tool just invoked, user typed `/clear`, session crashed), the harness's hook fires a short-lived `spt.exe <subcommand>` invocation that mutates on-disk SPT state (perch registry, spool, etc.). spt-core publishes this surface; harnesses author the bindings.
77:[Omitted long matching line]
113:[Omitted long matching line]
159:- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.
163:- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).
165:- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`).
168:[Omitted long matching line]
184:[Omitted long matching line]
229:A subagent's perch under a parent LiveAgent. Created on subagent start, torn down on subagent stop. Replaces today's "working perch" concept; first-class type so cross-communication between a Self and its workers (and worker↔worker) is addressable.
306:[Omitted long matching line]


Confirmed — CONTEXT.md:159: **`api bind` — post-spawn boot bind. Skeleton→live.** The skeleton perch is an existing concept. Let me read its full lifecycle definition:


Bash

sed -n '150,170p' CONTEXT.md; echo "=== Skeleton definition ==="; grep -n -i "skeleton" CONTEXT.md

Read skeleton perch lifecycle in CONTEXT


- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable.

### Inbound `api` surface (detailed)

All commands below are `api`-prefixed (machinery-facing). Every `api` invocation **and** every manifest carries a unified **`adapter_name`** string (e.g. `claude-spt`) identifying the owning adapter. This is load-bearing: one daemon hosts endpoints from multiple adapters (`claude-spt`, `spt-codex`, `spt-pi`), so the daemon resolves an endpoint's manifest + seams (history normalize-command, inject method, update avenue) by its `adapter_name`; adapter-update ripples target by it; capability lookup and telemetry key on it.

<!-- [doc->REQ-API-4] -->
**Manifest resolution from `--adapter` (since v0.8.0).** `spt api <cmd> --adapter <name[:profile]>` resolves the registered adapter's manifest, `:profile` overlay, and install dir from the registry when `--manifest` is omitted — a registered adapter's `api` calls need only `--adapter`. `--manifest <path>` becomes an optional **override** (an unregistered or local-dev manifest): when present, the manifest loads from that file and the install dir is its parent directory; when absent, both come from the registry record (the install dir is the record's precise `source_dir`). An unregistered adapter with no `--manifest` degrades to no-manifest rather than failing.

- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.
- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.
- **`api poll`** — short-lived drain of queued messages for a session (the hook-injection delivery path). `--include-deferred` optionally also drains deferred rows, for adapter flexibility (default excludes them — KNOWN-HAZARDS 1.4/4.4).
- **`api state <busy|idle>`** — adapter reports session activity; writes the activity/idle sentinel in the session perch. By default `api state idle` also writes the **echo-commune gate sentinel** (`.more-done`-equiv — modern spt couples them); `--no-gate` suppresses that coupling, and a standalone **`api echo-gate <set|clear>`** gives granular adapters explicit control over when echo communes may fire, independent of idle.
- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).
- **`api worker-poll`** — a Worker (subagent) receives its queued messages (inbound from Self or sibling Workers).
- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`).
- **`api session-end`** — session stop/crash report → soft teardown by default (preserve perch + spool + tracked history for recovery — KNOWN-HAZARDS 6.2). **`--erase`** instead hard-wipes the perch and tracked history (for ephemeral/secondary adapters that act as robust agent-spawned-agent surfaces).

**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own running id**. It is **node-local** — purge reaches only *this* node's records; a remote endpoint's records are unreachable and its subnet-registry rows decay on their own via the epoch-lease eviction. It removes: the perch directory **tree** recursively (incl every nested `{id}-psyche` / `{id}-w*` / shell child — info.json, ready marker, the `sessions.log` ledger, spool, the idle/echo-gate sentinels, the auth token); the registry address; the **context store** (`a-<id>` branch + worktree and the `<id>/` rows in every `p-<project>` branch — the same path `endpoint fork --delete-source` uses); and the node-local trust rows keyed on the id (access + visibility). Implementation-wise it is `fork --delete-source` generalized (recursive perch-remove + unregister + context `remove_endpoint`) plus the trust-record cleanup, sharing `endpoint rename`'s record-set enumeration and offline-only gate.
_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`).
<!-- [doc->REQ-ENDPOINT-PURGE] -->
=== Skeleton definition ===
113:**post-spawn seam** — the just-launched binary calls an spt-core command on boot (via the adapter's SessionStart-equivalent hook) to bind itself. Needed because the harness's own session id usually isn't known until after the binary runs. Payload: the harness `session_id` (when binary-generated rather than spt-core-injected); the `parent_pid` (the stable session-binding anchor — see KNOWN-HAZARDS 2.1); an endpoint identity/type confirmation; optionally a local HTTP port the binary listens on (for HTTP-mode input delivery, below); and a **boot nonce** (a generation/boot discriminator so a respawn-after-crash bind can't be confused with a stale duplicate — guards KNOWN-HAZARDS 2.4). The call flips the perch from skeleton → live.
159:- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.


CONTEXT.md:113 confirms the skeleton→live flip is the existing model. Let me verify whether `endpoint run` already writes the skeleton before spawn (determines if the fix is "add home to the existing skeleton step" vs "create the skeleton"):


1360	    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
1361	    // session history, and digest source relative to cwd.
1362	    //
1363	    // Resume cwd fallback chain (REQ-SESSION-RESUME-TEMPLATE): a caller-supplied
1364	    // cwd (the picker threads the selected Resume row's recorded cwd) → on a
1365	    // resume, the perch's `info.cwd` (W3 bind-cwd) → `current_dir` (today's
1366	    // single-project behavior + pre-migration ledger rows). A fresh bringup never
1367	    // consults `info.cwd` — it always lands in `current_dir` as before. A failure
1368	    // to read the cwd falls back to the broker-inherited cwd (None).
1369	    let project_cwd = cwd
1370	        .or_else(|| {
1371	            is_resume
1372	                .then(|| {
1373	                    let perch =
1374	                        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
1375	                    spt_store::info::read_info(&perch).and_then(|r| r.cwd)
1376	                })
1377	                .flatten()
1378	        })
1379	        .or_else(|| {
1380	            std::env::current_dir()
1381	                .ok()
1382	                .map(|p| p.to_string_lossy().into_owned())
1383	        });
1384	    match spt_daemon::harnesshost::launch_harness_brokered_in(
1385	        &spt_daemon::endpoint::broker_socket_name(),
1386	        id,
1387	        adapter,
1388	        &session_id,
1389	        &manifest,
1390	        project_cwd.as_deref(),
1391	        is_resume,
1392	        install_dir.as_deref(),
1393	    ) {
1394	        Ok(spawned) => {
1395	            // A TERSE machine line (no Rust `Some(..)` Debug leak, no "binds its
1396	            // perch on startup" internals — W4 UX: user-friendly, not a process
1397	            // log). `pid=<n>` stays a parseable machine token (scripts + e2e read
1398	            // the long-lived PTY child off it); a backend without a pid prints `-`.
1399	            let pid_display = spawned

    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
    // session history, and digest source relative to cwd.
    //
    // Resume cwd fallback chain (REQ-SESSION-RESUME-TEMPLATE): a caller-supplied
    // cwd (the picker threads the selected Resume row's recorded cwd) → on a
    // resume, the perch's `info.cwd` (W3 bind-cwd) → `current_dir` (today's
    // single-project behavior + pre-migration ledger rows). A fresh bringup never
    // consults `info.cwd` — it always lands in `current_dir` as before. A failure
    // to read the cwd falls back to the broker-inherited cwd (None).
    let project_cwd = cwd
        .or_else(|| {
            is_resume
                .then(|| {
                    let perch =
                        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
                    spt_store::info::read_info(&perch).and_then(|r| r.cwd)
                })
                .flatten()
        })
        .or_else(|| {
            std::env::current_dir()
                .ok()
                .map(|p| p.to_string_lossy().into_owned())
        });
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
        project_cwd.as_deref(),
        is_resume,
        install_dir.as_deref(),
    ) {
        Ok(spawned) => {
            // A TERSE machine line (no Rust `Some(..)` Debug leak, no "binds its
            // perch on startup" internals — W4 UX: user-friendly, not a process
            // log). `pid=<n>` stays a parseable machine token (scripts + e2e read
            // the long-lived PTY child off it); a backend without a pid prints `-`.
            let pid_display = spawned


Confirmed your point: the skeleton→live model is the existing concept (CONTEXT.md:113 "the call flips the perch from skeleton → live", :159). But I checked `cmd_endpoint_run` — for the **spt-hosted path it does NOT currently pre-create the skeleton**; it spawns straight to `launch_harness_brokered_in` (cli.rs:1384), and the harness's `bind` creates the perch fresh (establish_perch **None-branch** → `assign_home` → HOME_REFUSED on multi-subnet). So the fix is exactly your framing: **`endpoint run` creates the skeleton perch WITH the resolved home before spawning** → `bind` flips skeleton→live via the existing **prior-branch inherit** (no hook change, no env, reuses immutable-inherit). Not a new concept — using the one that exists.

**Crystallized v0.13.3 design so far:** home immutable (ADR-0010 stands); `endpoint run` resolves home at the skeleton-create step — single-subnet auto, multi-subnet: non-interactive → refuse (clear `--subnet` guidance, MRU-ordered), interactive → show proposed config (name/project/adapter[:profile]/home=MRU) + `Ok to proceed? Y/n` (n → `--subnet` guidance); `--subnet` always overrides; skeleton carries the home, bind inherits.

**Pausing the grill here** — todlando just split W3d and is **blocked on my design-gate for W3d-ii** (the intricate broker-restart refactor in the hardened broker), with W3d-i ready to gate. The v0.13.2 keystone is the critical path you told me to drive. Capturing the grill state, then pivoting; I'll resume the grill (remaining: skeleton lifecycle/cleanup, MRU storage, attach-ASAP, ADR) after the W3d gates.


TaskUpdate

LATENT GAP (perri, 2026-06-22, operator-hit) — NOT a version regression (perri pinned 0.11/0.12/0.12.1/0.13.1 all HOME_REFUSED; no-guess multi-subnet home policy established ≥0.11.0). spt-hosted bringup NEVER supported multi-subnet home; BIGNET 2nd-subnet crossing exposed it. `endpoint run` bind hook → HOME_REFUSED → silent no-perch → 25s timeout. Single-subnet unaffected. F-013 class.

DESIGN (grilled with operator 2026-06-22, grill PAUSED for v0.13.2 W3d — RESUME after):
- Home stays IMMUTABLE (ADR-0010 unchanged). Verified establish_perch: re-bind inherits recorded home (prior-branch) — gap is ONLY first-creation on multi-subnet.
- MECHANISM = reuse the EXISTING skeleton→live concept (CONTEXT:113/159). cmd_endpoint_run does NOT currently pre-create the skeleton for spt-hosted (spawns straight to launch_harness_brokered_in cli.rs:1384; bind creates fresh via establish_perch None-branch → assign_home → HOME_REFUSED). FIX: endpoint run creates the skeleton perch WITH the resolved home BEFORE spawn → bind flips skeleton→live via establish_perch prior-branch inherit (NO hook change, NO env-injection — supersedes my original 3-part env design, simpler). Skeleton Conflict-safe (empty session).
- HOME RESOLUTION at the skeleton-create step: single-subnet→auto (assign_home); multi-subnet no --subnet → NON-INTERACTIVE (stdin not TTY, use std::io::IsTerminal as cli.rs:1006/3956) REFUSE w/ clear --subnet guidance (MRU-ordered list); INTERACTIVE → print proposed config (name/project/adapter[:profile]/home=MRU-default) + "Ok to proceed? Y/n", n → --subnet guidance. --subnet always overrides + validates membership (assign_home NotMember).
- MRU: default home = most-recently-chosen subnet; persisted (location/scope TBD in grill).

STILL TO GRILL (paused): skeleton lifecycle (cleanup if harness never binds; list/picker visibility of a pre-bind skeleton), MRU storage/scope, attach-ASAP (decouple rc/attach from bind — PTY visible before bind, e.g. CC prompt blocking bind; user raised as coupled-but-distinct), whether an ADR is warranted (skeleton-with-home creation flow).

DISPOSITION: v0.13.3 EXPEDITE right after v0.13.2 (operator declined stopgap, staying blocked by choice). Mint REQ + dispatch todlando after v0.13.2. perri committing repro script + findings (regression test MUST run multi-subnet).


Updated task #8 description


Grill captured in task #8. Pivoting to the W3d keystone gate. Fetch + gate W3d-i (CLI-side CRC swap), then handle the W3d-ii design-gate todlando is blocked on:


Bash

git pull --ff-only --quiet 2>&1 | tail -1; echo "===HEAD==="; git log --oneline -1 | cat; echo "===W3d-i diff stat d6d7816..b62476e==="; git diff --stat d6d7816..b62476e | cat; echo "===cli.rs apply diff (impl only)==="; git diff d6d7816..b62476e -- crates/spt/src/cli.rs | grep -v "test\|assert\|#\[" | sed -n '1,90p'

Fetch + read W3d-i CLI-side CRC swap diff


===HEAD===
b62476e feat(cli): W3d-i adapter update via CRC swap, not remove+extract (ADR-0025)
===W3d-i diff stat d6d7816..b62476e===
 crates/spt/src/cli.rs | 152 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 145 insertions(+), 7 deletions(-)
===cli.rs apply diff (impl only)===
diff --git a/crates/spt/src/cli.rs b/crates/spt/src/cli.rs
index 9e1c658..5ca4b2d 100644
--- a/crates/spt/src/cli.rs
+++ b/crates/spt/src/cli.rs
@@ -6053,6 +6053,35 @@ fn cmd_adapter(action: AdapterCmd) -> i32 {
     }
 }

+/// Apply a verified staged release `.spt` into the install `dest` via a
+/// **content-hash swap** (W3d, ADR-0025): extract to a temp tree, then replace in
+/// `dest` ONLY the files whose content differs — via the never-strand atomic
+/// per-file commit ([`spt_daemon::crc_swap`]). Replaces the legacy
+/// `remove_dir_all(dest)` + blanket-extract, which on Windows fails the WHOLE
+/// update the instant a single install-dir binary is still running (the field
+/// brick). Identical files — and their still-running binaries — are left
+/// untouched. Additive: a file dropped by the new version is left in place
+/// (adapter file deletions are rare; a stale unreferenced file is harmless), so a
+/// running binary that vanished from the manifest is never yanked mid-update.
+// [impl->REQ-ADAPTER-LIVE-UPDATE]
+fn apply_release_crc_swap(
+    staged: &std::path::Path,
+    dest: &std::path::Path,
+) -> Result<(), String> {
+    // Extract to a sibling temp tree (platform-selective per W1), then diff+swap
+    // into `dest`. The temp tree is removed on every exit.
+    let staging = dest.with_extension("crc-stage");
+    let _ = std::fs::remove_dir_all(&staging);
+    std::fs::create_dir_all(&staging).map_err(|e| e.to_string())?;
+    let result = (|| {
+        extract_release_archive(staged, &staging).map_err(|e| e.to_string())?;
+        let plan = spt_daemon::crc_swap::plan_crc_swap(&staging, dest).map_err(|e| e.to_string())?;
+        spt_daemon::crc_swap::apply_crc_swap(&plan).map_err(|e| e.to_string())
+    })();
+    let _ = std::fs::remove_dir_all(&staging);
+    result
+}
+
 /// `spt adapter update [name]` (REQ-UPD-9) — the **CLI-side** gh_release update
 /// layer. For each registered adapter whose `[update] avenue = "gh_release"`
@@ -6172,23 +6201,23 @@ fn cmd_adapter_update(adapters: &std::path::Path, name: Option<&str>) -> i32 {
             );
         }

-        // 5. Extract + re-register (the verified/trusted bytes become the new
+        // 5. CRC-swap + re-register (the verified/trusted bytes become the new
         //    install in the durable _github home; pointer-mode re-reads it live).
+        //    W3d: a CONTENT-HASH swap (only changed files, never-strand) replaces
+        //    the legacy remove_dir_all + blanket-extract — which on Windows fails
+        //    the WHOLE update the moment one install-dir binary is still running.
         let safe = repo.replace(['/', '\\'], "-");
         let dest = adapters.join("_github").join(&safe);
-        if dest.exists() {
-            let _ = std::fs::remove_dir_all(&dest);
-        }
         if let Err(e) = std::fs::create_dir_all(&dest) {
             let _ = std::fs::remove_file(&staged);
             eprintln!("ADAPTER_UPDATE_FAIL:{}: create dest: {e}", record.name);
             failed = true;
             continue;
         }
-        let extracted = extract_release_archive(&staged, &dest);
+        let applied = apply_release_crc_swap(&staged, &dest);
         let _ = std::fs::remove_file(&staged);
-        if let Err(e) = extracted {
-            eprintln!("ADAPTER_UPDATE_FAIL:{}: extract: {e}", record.name);
+        if let Err(e) = applied {
+            eprintln!("ADAPTER_UPDATE_FAIL:{}: apply: {e}", record.name);
             failed = true;
             continue;
         }
             "the root binary extracts to dest/rolebin exactly as today"
         );
     }
+
+    // ---- v0.13.2 W3d-i: content-hash CRC swap of a staged release into dest ----
+
+    /// Recursively true iff `dir` contains any path component ending in one of the
+    /// transient swap/stage suffixes — `.crc-stage` / `.spt-stage` (the extract
+    /// temp trees) or `.new` / `.old` (the crc_swap commit-loop litter). Used to
+    /// prove a successful apply leaves NO temp residue under (or beside) `dest`.
+    fn has_swap_temp_residue(dir: &std::path::Path) -> bool {
+        let Ok(rd) = std::fs::read_dir(dir) else {
+            return false;
+        };
+        for entry in rd.flatten() {
+            let name = entry.file_name();
+            let name = name.to_string_lossy();
+            if name.ends_with(".crc-stage")
+                || name.ends_with(".spt-stage")
+                || name.ends_with(".new")


W3d-i clean: `apply_release_crc_swap` reuses W1 extract + W3b crc_swap into a sibling temp, removes the destructive `remove_dir_all`, additive (never yanks a vanished-from-manifest running binary). With (c) own-copy, even pre-ii the update is now non-destructive (safe rollback vs brick). Run gates:


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -2; echo "CLIPPY=${PIPESTATUS[0]}"
echo "=== W3d-i unit ==="; timeout 150 cargo test -p spt --bins apply_release_crc_swap 2>&1 | grep -E "test cli|test result" | tail; timeout 150 cargo test -p spt --bins crc_swap 2>&1 | grep -E "test result" | tail -3
echo "=== traceable ==="; "C:/Users/decid/AppData/Local/Programs/traceable-reqs/traceable-reqs" check >/tmp/td.txt 2>&1; echo "EXIT=$?"
echo "=== any W3d-ii plan doc on branch? ==="; ls W3*.md 2>/dev/null; git log --oneline -1 | cat

W3d-i gates + check for ii plan doc


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.99s
CLIPPY=0
=== W3d-i unit ===
test cli::tests::apply_release_crc_swap_only_swaps_changed ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 221 filtered out; finished in 0.10s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 221 filtered out; finished in 0.09s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
=== traceable ===
EXIT=0
=== any W3d-ii plan doc on branch? ===
W3-DESIGN-GATE.md
W3-DESIGN-PROPOSAL.md
W3-GATE-VERDICT.md
b62476e feat(cli): W3d-i adapter update via CRC swap, not remove+extract (ADR-0025)


**W3d-i = GATE-PASS** @b62476e (clippy 0, `apply_release_crc_swap_only_swaps_changed` pass, traceable 0). The W3d-ii plan came only over owl (truncated ~part 8) — I'll design-approve the shape with binding constraints and gate the actual diff carefully. Report + design-gate:


"$OWL" send todlando doyle <<'EOF'
W3d-i GATE-PASS @b62476e. apply_release_crc_swap reuses W1 extract + W3b swap into a sibling temp, drops the destructive remove_dir_all, additive (never yanks a vanished-from-manifest running binary). Good incremental state: even pre-ii the update is non-destructive (safe rollback vs brick). clippy 0, the only-swaps-changed unit passes, traceable 0. Clean scope-audit.

W3d-ii DESIGN-GATE: APPROVED to build with binding constraints. (Your owl plan truncated ~part 8 — I'm stating the required mechanics for the parts I couldn't see; if your restart/partial-failure design differs, FLAG it. I gate the actual W3d-ii diff carefully against these.)
- DETECT (CLI roster x adapter x alive): approved — none→W3d-i CLI-direct; some→broker.
- IPC: KIND_ADAPTER_APPLY + AdapterApplyReq{adapter,staging_dir,install_dir}, Brain::adapter_apply send+reply KIND_APPLIED/KIND_ERROR, dispatch_adapter_apply at handle_conn (KIND_KILL pattern), CLI sends staging PATH (same machine) + registry::register after — approved.
- LOCK DISCIPLINE (BINDING): clone Arc<Translation>, DROP the sessions Mutex BEFORE terminate() — NEVER hold the sessions lock across the bounded terminate() OR the crc swap, or you wedge every session op. Keep exactly this.
- build_translation factoring: approved IFF behavior-preserving for the existing dispatch_spawn path — I diff dispatch_spawn as a PURE extract (zero semantic change to the hardened spawn).
- manifest re-read for the NEW translation path (re-resolve message_idle_translation_binary.path from install_dir/manifest.toml post-swap): approved — verdict flag-5, restart must NOT reuse the stale cached path.
- RESTART RE-INSERT (BINDING): after rebuild, re-insert the new Arc<Translation> into HostedSession.translation under the sessions lock (replace the terminated one); session/PTY/OutputLog untouched (brain-parity). Confirm.
- PARTIAL-FAILURE (BINDING): swap fail → install_dir rolled-back-OLD (W3b never-strand) → you MUST re-spawn the translation from the OLD bytes (a terminated child can't un-terminate) + report failed; NEVER leave the session translation-less. Confirm you re-spawn-old on swap-fail.
- BOUNDED throughout (terminate 500ms, no unbounded waits); brain self-heals manifest via W3c reload (apply does NOT wait on it).

Build W3d-ii; ping at the gate. Then W3e — the NO-mocks keystone int vs perri's exact 4-step repro + the Windows locked-binary regression (the ship-blocker). Strong, disciplined work across W3.
EOF
echo "---sent $?---"

Report W3d-i pass + W3d-ii design-gate to todlando


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


Pivot done — W3d-i gated, W3d-ii design-approved with binding constraints (todlando building it now).

**v0.13.2 keystone progress:**

| Step | State |
|------|-------|
| W1 / W2 / W3a+W3b / W3c | ✅ GATE-PASS |
| W3 (a) stop-path reap + (c) psyche own-copy | ✅ GATE-PASS @d6d7816 (perri psyche-lock fix landed) |
| W3d-i CLI-side CRC swap | ✅ GATE-PASS @b62476e (update now non-destructive even pre-ii) |
| W3d-ii broker live-apply | 🔨 design-approved, building (careful gate on the diff) |
| W3e no-mocks keystone int | queued (perri's 4-step repro + Windows-lock regression = ship-blocker) |
| W4 / W5 / release | queued |

**Grill (v0.13.3 multi-subnet fix): paused, captured in task #8.** Resolved so far: home stays immutable; the fix reuses the **existing skeleton→live concept** (not new) — `endpoint run` creates the skeleton perch *with* the resolved home before spawn, bind inherits via the prior-branch (drops my original env design entirely); home resolution = single-auto / multi-subnet non-interactive-refuse / interactive-confirm-MRU / `--subnet` override. Remaining to grill: skeleton lifecycle+cleanup, MRU storage, attach-ASAP, ADR.

**To resume the grill, just say so** (or reply with the next answer). Otherwise I keep driving v0.13.2 to release. Reachable on `doyle`.


proceed with grill


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


Resuming the grill. Next decision: **attach-ASAP** (your "necessary change") — it's the most consequential remaining piece and couples to the skeleton. Grounding the current attach sequencing first:


1420	                &format!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)"),
1421	                crate::helpfmt::stderr_color()
1422	            )
1423	        );
1424	        return 0;
1425	    }
1426	    // Await the bringup ONLINE before attaching (REQ-HAZARD-RC-ATTACH-ONLINE-RACE):
1427	    // launch_harness_brokered_in returns once the harness PROCESS is spawned, but the
1428	    // broker-PTY bind (status -> ONLINE + a live session) lands ASYNC. An immediate
1429	    // run_attach loses the race three ways — connects to an empty ring then
1430	    // EOF-detaches, hits the offline status-gate, or finds no live session yet. FAIL
1431	    // LOUD on timeout rather than fall through to a doomed attach. Only the
1432	    // offline-bringup (Run) path reaches here; an already-online endpoint routes
1433	    // Outcome::Attach -> rc::run_attach and never double-counts this wait.
1434	    if !await_endpoint_online(id) {
1435	        eprintln!(
1436	            "ENDPOINT_RUN_ONLINE_TIMEOUT:{id}: harness did not come online within {ONLINE_AWAIT_SECS}s \
1437	             — not attaching (check `spt endpoint list`; retry with `spt rc {id}`)"
1438	        );
1439	        return 1;
1440	    }
1441	    match crate::rc::run_attach(id, rc_intent(view, false)) {
1442	        Ok(()) => 0,
1443	        Err(e) => {
1444	            eprintln!("RC_FAIL:{id}: {e}");
1445	            1
1446	        }
1447	    }
1448	}
1449
1450	/// The harness-boot deadline `cmd_endpoint_run` waits for a freshly-spawned
1451	/// endpoint to come ONLINE before attaching (REQ-HAZARD-RC-ATTACH-ONLINE-RACE).
1452	const ONLINE_AWAIT_SECS: u64 = 25;
1453
1454	/// Poll the freshly-spawned endpoint's perch status to [`STATUS_ONLINE`], bounded
1455	/// by [`ONLINE_AWAIT_SECS`] — the await-online gate before an attach/view bringup.
1456	/// Returns `true` once online, `false` on timeout (the caller then fails loud).
1457	// [impl->REQ-HAZARD-RC-ATTACH-ONLINE-RACE]
1458	fn await_endpoint_online(id: &str) -> bool {
1459	    use std::time::Duration;
1460	    const INTERVAL: Duration = Duration::from_millis(150);
1461	    let attempts = (ONLINE_AWAIT_SECS * 1000 / INTERVAL.as_millis() as u64) as u32;
1462	    let perch_path =
1463	        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
1464	    poll_online(
1465	        || {
1466	            spt_store::info::read_info(&perch_path)
1467	                .and_then(|i| i.status)
1468	                .as_deref()
1469	                == Some(spt_store::liveness::STATUS_ONLINE)
1470	        },
1471	        attempts,
1472	        || std::thread::sleep(INTERVAL),
1473	    )
1474	}
1475
1476	/// Bounded poll combinator: invoke `is_online` up to `attempts` times, sleeping
1477	/// `sleep` BETWEEN failed checks (never after the last); returns `true` the first
1478	/// time it reports online (zero sleeps when already online), `false` if it never
1479	/// does within the budget. The store read + the sleep are INJECTED so the

                &format!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)"),
                crate::helpfmt::stderr_color()
            )
        );
        return 0;
    }
    // Await the bringup ONLINE before attaching (REQ-HAZARD-RC-ATTACH-ONLINE-RACE):
    // launch_harness_brokered_in returns once the harness PROCESS is spawned, but the
    // broker-PTY bind (status -> ONLINE + a live session) lands ASYNC. An immediate
    // run_attach loses the race three ways — connects to an empty ring then
    // EOF-detaches, hits the offline status-gate, or finds no live session yet. FAIL
    // LOUD on timeout rather than fall through to a doomed attach. Only the
    // offline-bringup (Run) path reaches here; an already-online endpoint routes
    // Outcome::Attach -> rc::run_attach and never double-counts this wait.
    if !await_endpoint_online(id) {
        eprintln!(
            "ENDPOINT_RUN_ONLINE_TIMEOUT:{id}: harness did not come online within {ONLINE_AWAIT_SECS}s \
             — not attaching (check `spt endpoint list`; retry with `spt rc {id}`)"
        );
        return 1;
    }
    match crate::rc::run_attach(id, rc_intent(view, false)) {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("RC_FAIL:{id}: {e}");
            1
        }
    }
}

/// The harness-boot deadline `cmd_endpoint_run` waits for a freshly-spawned
/// endpoint to come ONLINE before attaching (REQ-HAZARD-RC-ATTACH-ONLINE-RACE).
const ONLINE_AWAIT_SECS: u64 = 25;

/// Poll the freshly-spawned endpoint's perch status to [`STATUS_ONLINE`], bounded
/// by [`ONLINE_AWAIT_SECS`] — the await-online gate before an attach/view bringup.
/// Returns `true` once online, `false` on timeout (the caller then fails loud).
// [impl->REQ-HAZARD-RC-ATTACH-ONLINE-RACE]
fn await_endpoint_online(id: &str) -> bool {
    use std::time::Duration;
    const INTERVAL: Duration = Duration::from_millis(150);
    let attempts = (ONLINE_AWAIT_SECS * 1000 / INTERVAL.as_millis() as u64) as u32;
    let perch_path =
        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);
    poll_online(
        || {
            spt_store::info::read_info(&perch_path)
                .and_then(|i| i.status)
                .as_deref()
                == Some(spt_store::liveness::STATUS_ONLINE)
        },
        attempts,
        || std::thread::sleep(INTERVAL),
    )
}

/// Bounded poll combinator: invoke `is_online` up to `attempts` times, sleeping
/// `sleep` BETWEEN failed checks (never after the last); returns `true` the first
/// time it reports online (zero sleeps when already online), `false` if it never
/// does within the budget. The store read + the sleep are INJECTED so the


Current sequencing confirmed: `cmd_endpoint_run` → spawn → **`await_endpoint_online` (polls perch STATUS_ONLINE = bind)** → `run_attach` (cli.rs:1434). The comment even lists the 3 failure modes of attaching early (empty-ring EOF, offline status-gate, no live session yet) — that's exactly why it waits for bind. But the broker **session/PTY exists at spawn, before bind**, so the gate can shift earlier. Your deadlock: a harness prompt that blocks bind → operator can't see it (no attach) → bind never lands → 25s timeout.

Question 4 (attach-ASAP):


AskUserQuestion

Question: attach-ASAP: how should `endpoint run` decide when to attach, so the PTY is visible before bind (letting the operator clear a bind-gating prompt)?

Choices:
Replace await_endpoint_online (perch STATUS_ONLINE = bind) with await-session-attachable: the broker creates the session+PTY+OutputLog at spawn, BEFORE bind, so attach as soon as that session exists — regardless of perch-online. run_attach attaches to the live session even when the perch is still offline/skeleton. Preserves REQ-HAZARD-RC-ATTACH-ONLINE-RACE's 'no attach before there's a session' intent, just at the earlier session-exists point. Operator sees + drives the harness immediately (incl clearing a bind-gating prompt).
spawn (broker creates session+PTY immediately)
  → await SESSION attachable (not perch-online)
  → run_attach → operator sees harness NOW
     → clears the CC startup prompt
        → hook fires bind → perch goes online
NET: gate moves bind→session-exists; no deadlock
Keep waiting for perch-online, but after a short grace with no bind, attach anyway to whatever session exists. Smaller change, but it's a heuristic timer (race-prone) layered on the existing gate rather than fixing the gate's basis.
spawn → await online (Ns grace)
  → if still offline but a session exists → attach anyway
  → else keep waiting / timeout
NET: timer heuristic; still bind-centric, race-prone

Question: Scope: attach-ASAP relative to the multi-subnet home fix?

Choices:
Distinct REQ (attach sequencing) from the home-resolution REQ, but shipped together in v0.13.3 — they form the coherent endpoint-run creation-flow fix. Each gets its own tag + test.
v0.13.3:
  REQ-A: multi-subnet home (skeleton w/ home)
  REQ-B: attach-on-session-exists
  — separate REQs, separate tests, one release
Treat the whole endpoint-run creation-flow rework (home resolve + skeleton + attach-ASAP) as one REQ. Simpler bookkeeping but a fuzzier, larger requirement that's harder to test/trace in isolation.
v0.13.3:
  REQ-X: endpoint-run creation flow
   (home + skeleton + attach) — one big REQ


Your questions have been answered: "attach-ASAP: how should `endpoint run` decide when to attach, so the PTY is visible before bind (letting the operator clear a bind-gating prompt)?"=(no option selected) notes: gate on broker-session-attachable. note it should also be possible to attach PTY in this state via `spt rc`, for any headless endpoints that are brought up. we probably need to mint a new endpoint lifecycle state for this-->"Unbound" (amber. or is there already an amber state?), "Scope: attach-ASAP relative to the multi-subnet home fix?"="Separate REQ, same v0.13.3" selected preview:
v0.13.3:
  REQ-A: multi-subnet home (skeleton w/ home)
  REQ-B: attach-on-session-exists
  — separate REQs, separate tests, one release. You can now continue with these answers in mind.

Question: attach-ASAP: how should `endpoint run` decide when to attach, so the PTY is visible before bind (letting the operator clear a bind-gating prompt)?

Choices:
Replace await_endpoint_online (perch STATUS_ONLINE = bind) with await-session-attachable: the broker creates the session+PTY+OutputLog at spawn, BEFORE bind, so attach as soon as that session exists — regardless of perch-online. run_attach attaches to the live session even when the perch is still offline/skeleton. Preserves REQ-HAZARD-RC-ATTACH-ONLINE-RACE's 'no attach before there's a session' intent, just at the earlier session-exists point. Operator sees + drives the harness immediately (incl clearing a bind-gating prompt).
spawn (broker creates session+PTY immediately)
  → await SESSION attachable (not perch-online)
  → run_attach → operator sees harness NOW
     → clears the CC startup prompt
        → hook fires bind → perch goes online
NET: gate moves bind→session-exists; no deadlock
Keep waiting for perch-online, but after a short grace with no bind, attach anyway to whatever session exists. Smaller change, but it's a heuristic timer (race-prone) layered on the existing gate rather than fixing the gate's basis.
spawn → await online (Ns grace)
  → if still offline but a session exists → attach anyway
  → else keep waiting / timeout
NET: timer heuristic; still bind-centric, race-prone

Question: Scope: attach-ASAP relative to the multi-subnet home fix?

Choices:
Distinct REQ (attach sequencing) from the home-resolution REQ, but shipped together in v0.13.3 — they form the coherent endpoint-run creation-flow fix. Each gets its own tag + test.
v0.13.3:
  REQ-A: multi-subnet home (skeleton w/ home)
  REQ-B: attach-on-session-exists
  — separate REQs, separate tests, one release
Treat the whole endpoint-run creation-flow rework (home resolve + skeleton + attach-ASAP) as one REQ. Simpler bookkeeping but a fuzzier, larger requirement that's harder to test/trace in isolation.
v0.13.3:
  REQ-X: endpoint-run creation flow
   (home + skeleton + attach) — one big REQ


W2 — gh transport for private repos

W4 — update-message

Release — cut v0.13.2 (deployah role)

LATENT GAP (perri, 2026-06-22, operator-hit) — NOT a version regression (perri pinned 0.11/0.12/0.12.1/0.13.1 all HOME_REFUSED; no-guess multi-subnet home policy established ≥0.11.0). spt-hosted bringup NEVER supported multi-subnet home; BIGNET 2nd-subnet crossing exposed it. `endpoint run` bind hook → HOME_REFUSED → silent no-perch → 25s timeout. Single-subnet unaffected. F-013 class.

DESIGN (grilled with operator 2026-06-22, grill PAUSED for v0.13.2 W3d — RESUME after):
- Home stays IMMUTABLE (ADR-0010 unchanged). Verified establish_perch: re-bind inherits recorded home (prior-branch) — gap is ONLY first-creation on multi-subnet.
- MECHANISM = reuse the EXISTING skeleton→live concept (CONTEXT:113/159). cmd_endpoint_run does NOT currently pre-create the skeleton for spt-hosted (spawns straight to launch_harness_brokered_in cli.rs:1384; bind creates fresh via establish_perch None-branch → assign_home → HOME_REFUSED). FIX: endpoint run creates the skeleton perch WITH the resolved home BEFORE spawn → bind flips skeleton→live via establish_perch prior-branch inherit (NO hook change, NO env-injection — supersedes my original 3-part env design, simpler). Skeleton Conflict-safe (empty session).
- HOME RESOLUTION at the skeleton-create step: single-subnet→auto (assign_home); multi-subnet no --subnet → NON-INTERACTIVE (stdin not TTY, use std::io::IsTerminal as cli.rs:1006/3956) REFUSE w/ clear --subnet guidance (MRU-ordered list); INTERACTIVE → print proposed config (name/project/adapter[:profile]/home=MRU-default) + "Ok to proceed? Y/n", n → --subnet guidance. --subnet always overrides + validates membership (assign_home NotMember).
- MRU: default home = most-recently-chosen subnet; persisted (location/scope TBD in grill).

STILL TO GRILL (paused): skeleton lifecycle (cleanup if harness never binds; list/picker visibility of a pre-bind skeleton), MRU storage/scope, attach-ASAP (decouple rc/attach from bind — PTY visible before bind, e.g. CC prompt blocking bind; user raised as coupled-but-distinct), whether an ADR is warranted (skeleton-with-home creation flow).

DISPOSITION: v0.13.3 EXPEDITE right after v0.13.2 (operator declined stopgap, staying blocked by choice). Mint REQ + dispatch todlando after v0.13.2. perri committing repro script + findings (regression test MUST run multi-subnet).


Gate on broker-session-attachable ✓; `spt rc` should attach in that pre-bind state too (for headless bringups); separate REQs in v0.13.3 ✓. You asked: is there already an amber state? Let me ground the existing status/color model before answering:


Bash

echo "=== liveness STATUS_* constants ==="; grep -rn "STATUS_\|pub const STATUS" crates/spt-store/src/liveness.rs | head -20
echo "=== amber / picker color derivation ==="; grep -rn "amber\|Amber\|green\|Green\|grey\|gray\|yellow\|Color::\|status_color\|fn.*color" crates/spt/src/ crates/spt-store/src 2>/dev/null | grep -i "amber\|green\|grey\|gray\|yellow\|status.*color\|color.*status" | head -20

Ground status constants + amber/color model


=== liveness STATUS_* constants ===
37:pub const STATUS_ONLINE: &str = "online";
39:pub const STATUS_OFFLINE: &str = "offline";
84:                return status == STATUS_ONLINE; // daemon-authoritative; never pid
106:        Some(status) => status == STATUS_ONLINE, // daemon-hosted: authoritative
136:        set_status(d.path(), STATUS_ONLINE).unwrap();
153:        set_status(d.path(), STATUS_OFFLINE).unwrap();
223:        set_status(&off, STATUS_OFFLINE).unwrap();
233:        set_status(&on, STATUS_ONLINE).unwrap();
=== amber / picker color derivation ===
crates/spt/src/api/startup.rs:459:    // amber-paints it regardless.
crates/spt/src/api/startup.rs:756:    // None (the picker's type-gate renders it plain online, never amber).
crates/spt/src/cli.rs:235:    /// blurb (the resource-registry yellow-pages projection).
crates/spt/src/cli.rs:1755:/// blurbs (the ex-`resources list` yellow-pages projection, decision 25c);
crates/spt/src/cli.rs:8532:        std::fs::write(&file, "Guard the temple. Keep the fleet green.\n").unwrap();
crates/spt/src/cli.rs:8540:            "Guard the temple. Keep the fleet green."
crates/spt/src/picker/data.rs:254:                // remote row can't determine it, so it renders plain online/green —
crates/spt/src/picker/data.rs:255:                // amber "harness-only" is never inferred across the subnet
crates/spt/src/picker/model.rs:19:    /// Online endpoint — filled square (rendered green in the view).
crates/spt/src/picker/model.rs:21:    /// Offline endpoint — hollow square (rendered gray in the view).
crates/spt/src/picker/model.rs:58:    /// Green — online + broker-PTY-controllable (spt-hosted), free.
crates/spt/src/picker/model.rs:60:    /// Amber — online LiveAgent that is NOT broker-PTY-controllable (harness-hosted,
crates/spt/src/picker/model.rs:251:    /// type-gate for the amber "harness-only" display state (amber is LiveAgent-only).
crates/spt/src/picker/model.rs:256:    /// The four-state DISPLAY status (REQ-PICKER-1), type-gated so amber
crates/spt/src/picker/model.rs:258:    /// PTY-attachable agent — renders green when online regardless of
crates/spt/src/picker/model.rs:259:    /// controllability. Precedence: offline → gray; a controlled endpoint → blue;
crates/spt/src/picker/model.rs:260:    /// a non-LiveAgent → green; a LiveAgent that is not controllable (`Some(false)`
crates/spt/src/picker/model.rs:261:    /// or a legacy `None`, which self-corrects on the next bind) → amber; else
crates/spt/src/picker/model.rs:262:    /// (a controllable LiveAgent) → green.
crates/spt/src/picker/model.rs:271:        // amber is LiveAgent-only — a gateway/shell online is plain green.


`STATUS_ONLINE`/`STATUS_OFFLINE` are the only on-disk statuses. But the picker has a **4-state DISPLAY model** — and **amber is already taken**: it means "online LiveAgent NOT broker-PTY-controllable (harness-hosted)." Let me read the full model to ground the "Unbound" question:


50	/// colour and the pick-existing details STATUS line (REQ-PICKER-1). Derived from
51	/// the raw online/offline ([`EpStatus`]) plus broker-PTY-controllability, the
52	/// remote-control marker, and the endpoint type. Distinct from [`EpStatus`] (the
53	/// raw duality the confirm-layer logic keys on).
54	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55	pub enum EpDisplay {
56	    /// Gray — offline.
57	    Offline,
58	    /// Green — online + broker-PTY-controllable (spt-hosted), free.
59	    Online,
60	    /// Amber — online LiveAgent that is NOT broker-PTY-controllable (harness-hosted,
61	    /// no broker PTY seat).
62	    HarnessOnly,
63	    /// Blue — online + a node currently controls it (`driven_by`).
64	    Controlled,
65	}
66
67	impl EpDisplay {
68	    /// The STATUS-line / legend label.
69	    pub fn label(self) -> &'static str {

/// colour and the pick-existing details STATUS line (REQ-PICKER-1). Derived from
/// the raw online/offline ([`EpStatus`]) plus broker-PTY-controllability, the
/// remote-control marker, and the endpoint type. Distinct from [`EpStatus`] (the
/// raw duality the confirm-layer logic keys on).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EpDisplay {
    /// Gray — offline.
    Offline,
    /// Green — online + broker-PTY-controllable (spt-hosted), free.
    Online,
    /// Amber — online LiveAgent that is NOT broker-PTY-controllable (harness-hosted,
    /// no broker PTY seat).
    HarnessOnly,
    /// Blue — online + a node currently controls it (`driven_by`).
    Controlled,
}

impl EpDisplay {
    /// The STATUS-line / legend label.
    pub fn label(self) -> &'static str {


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/11" id="34946670" type="msg" from="todlando">W3d-ii DONE @322ca55 (pushed). The broker-coordinated live apply — perri's keystone. Ready for your careful cross-process gate. Confirming each binding constraint:

- LOCK DISCIPLINE (your binding): CONFIRMED. dispatch_adapter_apply holds the sessions Mutex ONLY for (1) the affected-sessions snapshot, (2) the</EVENT-PART>
<EVENT-PART seq="2/11" id="34946670"> per-session input Arc fetch, (3) the translation re-insert. The lock is DROPPED before terminate() AND before the crc swap. build_translation (spawns a process + worker thread) runs OFF-lock.
- build_translation factor: CONFIRMED pure extract — dispatch_spawn's translation block moved verbatim into the fn; dispatch_spawn now calls build_</EVENT-PART>
<EVENT-PART seq="3/11" id="34946670">translation(path, &amp;req.endpoint, &amp;input). Zero semantic change (diff it). Reuses the session's existing Arc&lt;InputWriter&gt;/PTY/log.
- manifest re-read (flag-5): CONFIRMED. read_translation_path reads install_dir/manifest.toml -&gt; message_idle_translation_binary.path fresh post-swap; the restart NEVER reuses the stale cached pa</EVENT-PART>
<EVENT-PART seq="4/11" id="34946670">th.
- RESTART RE-INSERT (your binding): CONFIRMED. After rebuild, the new Arc&lt;Translation&gt; is re-inserted into HostedSession.translation under the sessions lock (replacing the terminated one); session/PTY/OutputLog untouched (brain-parity).
- PARTIAL-FAILURE (your binding): CONFIRMED re-spawn-OLD. On swap-fail, install_dir is rolle</EVENT-PART>
<EVENT-PART seq="5/11" id="34946670">d back to OLD bytes (W3b never-strand), restart_path = the OLD pre-swap path, every affected session is re-spawned from it, then the apply returns Err(reported). NEVER stopped-without-restart / never translation-less.
- BOUNDED: terminate() 500ms; no unbounded broker waits. Brain self-heals manifest via W3c reload-on-dirty; the apply does N</EVENT-PART>
<EVENT-PART seq="6/11" id="34946670">OT wait on it. CLI reports on the broker-side applied/error reply.

SIMPLIFICATION (flagged earlier, applied): with (c) own-copy the translation binary is the ONLY install-dir locker, so the apply cycles ONLY the translation — NO psyche stop/reap (your original ripple's &quot;reap psyche&quot; is unnecessary; the psyche runs from .live</EVENT-PART>
<EVENT-PART seq="7/11" id="34946670">-bin and picks up new bytes on next respawn).

IPC: KIND_ADAPTER_APPLY + AdapterApplyReq{adapter, staging_dir, install_dir} (msg.rs); Brain::adapter_apply (send + block for KIND_APPLIED/KIND_ERROR, frames matched by KIND since the applied reply has no payload); CLI extracts staged-&gt;staging tree, sends the staging PATH (same machine) +</EVENT-PART>
<EVENT-PART seq="8/11" id="34946670"> registry::register after. CLI routing: adapter_has_live_endpoint (roster x info.adapter parent-split x daemon-auth liveness) -&gt; none-live = W3d-i CLI-direct crc; some-live = apply_release_via_d
...(truncated)</event>
</task-notification>


Grounded — and this surfaces a semantics conflict to flag (per the grill contract): **amber is already taken.** `EpDisplay` has 4 states: Offline (gray), Online (green), **HarnessOnly (amber) = online LiveAgent NOT broker-PTY-controllable**, Controlled (blue). Crucially, amber signals *"not broker-controllable."* But your "Unbound" endpoint (spt-hosted, broker spawned its PTY) **IS** broker-attachable — it just hasn't bound yet. So amber would mean the opposite of what Unbound is.

Question 5 — the Unbound state:


AskUserQuestion

Question: How should 'Unbound' (broker session/PTY live, perch not yet bound) be represented?

Choices:
Not a new on-disk status. Derived in the picker/list: perch-not-online + a LIVE broker session exists (source = the broker sessions map, the W3a endpoint field) → render Unbound. Same source powers REQ-B's `spt rc` attach-on-session-exists. Local-only (a remote row can't see another node's broker, renders plain — consistent with the existing controllability limitation, picker/data.rs:254).
broker sessions map = source of truth
  perch offline + live session → Unbound (display)
  perch online            → Online/Amber/Blue
  perch offline, no session → Offline
rc <id>: attach iff live session (REQ-B)
NET: no new on-disk status; one source
Daemon stamps a STATUS_UNBOUND on the perch at spawn, flipped to ONLINE at bind. Explicit + queryable, but adds a third on-disk status to the daemon-authoritative liveness model (STATUS_ONLINE/OFFLINE) + more reconcile/teardown edges to keep honest.
spawn → set_status(UNBOUND)
bind  → set_status(ONLINE)
death → set_status(OFFLINE)
NET: 3rd on-disk status; new reconcile edges

Question: Color/term for Unbound (amber=HarnessOnly is taken, and means 'not controllable' — the opposite of Unbound, which IS attachable)?

Choices:
A distinct 5th color, semantically clean: cyan reads as 'live/attachable, transitional' — not amber's 'not-controllable' meaning. Label 'Unbound'.
gray=Offline  green=Online  amber=HarnessOnly
blue=Controlled  CYAN=Unbound (attachable,
                              not yet bound)
Unbound IS broker-controllable (like green) but transient — a dim or hollow-green variant signals 'green-ish, attachable, not settled.' Ties it visually to its true nature (attachable) rather than a new hue.
green=Online (filled)
DIM/HOLLOW green=Unbound (attachable, transient)
amber=HarnessOnly  blue=Controlled  gray=Offline


Your questions have been answered: "How should 'Unbound' (broker session/PTY live, perch not yet bound) be represented?"="New on-disk status" selected preview:
spawn → set_status(UNBOUND)
bind  → set_status(ONLINE)
death → set_status(OFFLINE)
NET: 3rd on-disk status; new reconcile edges, "Color/term for Unbound (amber=HarnessOnly is taken, and means 'not controllable' — the opposite of Unbound, which IS attachable)?"=(no option selected) notes: hollow is a good precedent. and technically, there would be need for a "controlled" variant of it.. You can now continue with these answers in mind.

Question: How should 'Unbound' (broker session/PTY live, perch not yet bound) be represented?

Choices:
Not a new on-disk status. Derived in the picker/list: perch-not-online + a LIVE broker session exists (source = the broker sessions map, the W3a endpoint field) → render Unbound. Same source powers REQ-B's `spt rc` attach-on-session-exists. Local-only (a remote row can't see another node's broker, renders plain — consistent with the existing controllability limitation, picker/data.rs:254).
broker sessions map = source of truth
  perch offline + live session → Unbound (display)
  perch online            → Online/Amber/Blue
  perch offline, no session → Offline
rc <id>: attach iff live session (REQ-B)
NET: no new on-disk status; one source
Daemon stamps a STATUS_UNBOUND on the perch at spawn, flipped to ONLINE at bind. Explicit + queryable, but adds a third on-disk status to the daemon-authoritative liveness model (STATUS_ONLINE/OFFLINE) + more reconcile/teardown edges to keep honest.
spawn → set_status(UNBOUND)
bind  → set_status(ONLINE)
death → set_status(OFFLINE)
NET: 3rd on-disk status; new reconcile edges

Question: Color/term for Unbound (amber=HarnessOnly is taken, and means 'not controllable' — the opposite of Unbound, which IS attachable)?

Choices:
A distinct 5th color, semantically clean: cyan reads as 'live/attachable, transitional' — not amber's 'not-controllable' meaning. Label 'Unbound'.
gray=Offline  green=Online  amber=HarnessOnly
blue=Controlled  CYAN=Unbound (attachable,
                              not yet bound)
Unbound IS broker-controllable (like green) but transient — a dim or hollow-green variant signals 'green-ish, attachable, not settled.' Ties it visually to its true nature (attachable) rather than a new hue.
green=Online (filled)
DIM/HOLLOW green=Unbound (attachable, transient)
amber=HarnessOnly  blue=Controlled  gray=Offline


Prompt:
<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/11" id="34946670" type="msg" from="todlando">W3d-ii DONE @322ca55 (pushed). The broker-coordinated live apply — perri's keystone. Ready for your careful cross-process gate. Confirming each binding constraint:

- LOCK DISCIPLINE (your binding): CONFIRMED. dispatch_adapter_apply holds the sessions Mutex ONLY for (1) the affected-sessions snapshot, (2) the</EVENT-PART>
<EVENT-PART seq="2/11" id="34946670"> per-session input Arc fetch, (3) the translation re-insert. The lock is DROPPED before terminate() AND before the crc swap. build_translation (spawns a process + worker thread) runs OFF-lock.
- build_translation factor: CONFIRMED pure extract — dispatch_spawn's translation block moved verbatim into the fn; dispatch_spawn now calls build_</EVENT-PART>
<EVENT-PART seq="3/11" id="34946670">translation(path, &amp;req.endpoint, &amp;input). Zero semantic change (diff it). Reuses the session's existing Arc&lt;InputWriter&gt;/PTY/log.
- manifest re-read (flag-5): CONFIRMED. read_translation_path reads install_dir/manifest.toml -&gt; message_idle_translation_binary.path fresh post-swap; the restart NEVER reuses the stale cached pa</EVENT-PART>
<EVENT-PART seq="4/11" id="34946670">th.
- RESTART RE-INSERT (your binding): CONFIRMED. After rebuild, the new Arc&lt;Translation&gt; is re-inserted into HostedSession.translation under the sessions lock (replacing the terminated one); session/PTY/OutputLog untouched (brain-parity).
- PARTIAL-FAILURE (your binding): CONFIRMED re-spawn-OLD. On swap-fail, install_dir is rolle</EVENT-PART>
<EVENT-PART seq="5/11" id="34946670">d back to OLD bytes (W3b never-strand), restart_path = the OLD pre-swap path, every affected session is re-spawned from it, then the apply returns Err(reported). NEVER stopped-without-restart / never translation-less.
- BOUNDED: terminate() 500ms; no unbounded broker waits. Brain self-heals manifest via W3c reload-on-dirty; the apply does N</EVENT-PART>
<EVENT-PART seq="6/11" id="34946670">OT wait on it. CLI reports on the broker-side applied/error reply.

SIMPLIFICATION (flagged earlier, applied): with (c) own-copy the translation binary is the ONLY install-dir locker, so the apply cycles ONLY the translation — NO psyche stop/reap (your original ripple's &quot;reap psyche&quot; is unnecessary; the psyche runs from .live</EVENT-PART>
<EVENT-PART seq="7/11" id="34946670">-bin and picks up new bytes on next respawn).

IPC: KIND_ADAPTER_APPLY + AdapterApplyReq{adapter, staging_dir, install_dir} (msg.rs); Brain::adapter_apply (send + block for KIND_APPLIED/KIND_ERROR, frames matched by KIND since the applied reply has no payload); CLI extracts staged-&gt;staging tree, sends the staging PATH (same machine) +</EVENT-PART>
<EVENT-PART seq="8/11" id="34946670"> registry::register after. CLI routing: adapter_has_live_endpoint (roster x info.adapter parent-split x daemon-auth liveness) -&gt; none-live = W3d-i CLI-direct crc; some-live = apply_release_via_d
...(truncated)</event>
</task-notification>


Q5 captured: **`STATUS_UNBOUND`** new on-disk status (spawn→UNBOUND, bind→ONLINE, death→OFFLINE); display = **hollow** treatment (distinct from filled-green Online; amber is taken + means *not*-controllable), with a **controlled-variant** of hollow too. attach-ASAP/`rc` gate on broker-session-exists; Unbound endpoints are attachable.

The W3d-ii keystone just landed (@322ca55, all binding constraints confirmed + a clean simplification: with (c) own-copy, only the translation locks install_dir → apply cycles *only* the translation, no psyche reap). That's the perri-fix landing on the critical path you're driving to release. Capturing the grill + pausing once more to gate it carefully (3 small grill items left: skeleton cleanup, MRU storage, ADR):


TaskUpdate

LATENT GAP (perri, 2026-06-22) — NOT a version regression (≥0.11.0 all HOME_REFUSED). spt-hosted bringup never supported multi-subnet home; BIGNET 2nd-subnet crossing exposed it. Single-subnet unaffected. F-013 class. v0.13.3 EXPEDITE (operator declined stopgap, blocked by choice).

TWO REQs (grilled with operator 2026-06-22; grill ~80%, PAUSED for v0.13.2 W3d-ii keystone gate):

REQ-A — multi-subnet home resolution:
- Home IMMUTABLE (ADR-0010). Gap is ONLY first-creation on multi-subnet (re-bind inherits via establish_perch prior-branch).
- MECHANISM: reuse existing skeleton→live (CONTEXT:113/159). endpoint run creates the skeleton perch WITH resolved home BEFORE spawn (currently does NOT — spawns straight to launch_harness_brokered_in cli.rs:1384, bind creates fresh None-branch → assign_home → HOME_REFUSED). bind flips skeleton→live inheriting home (prior-branch). NO hook change, NO env (supersedes original 3-part env design).
- HOME RESOLUTION at skeleton-create: single→auto; multi no --subnet → NON-INTERACTIVE refuse (std::io::IsTerminal, cli.rs:1006/3956) w/ MRU-ordered --subnet guidance; INTERACTIVE → print proposed config (name/project/adapter[:profile]/home=MRU) + "Ok to proceed? Y/n", n→--subnet guidance. --subnet overrides+validates (assign_home NotMember).
- MRU: default home = most-recently-chosen subnet, persisted (location/scope TBD - grill).

REQ-B — attach-on-session-exists + Unbound state:
- Current: cmd_endpoint_run awaits perch STATUS_ONLINE (await_endpoint_online cli.rs:1434) before attach → deadlocks when a harness prompt blocks bind (operator can't see it).
- FIX: gate attach on the BROKER SESSION being attachable (broker creates session+PTY+OutputLog at spawn, BEFORE bind), not perch-online. run_attach + `spt rc <id>` attach to a live session regardless of perch status (works for headless bringups too). Source = broker sessions map (W3a endpoint field). Local-only (remote rows render plain, like controllability).
- NEW on-disk status STATUS_UNBOUND (operator chose on-disk over derived): spawn→UNBOUND, bind→ONLINE, death→OFFLINE. New reconcile/teardown edges to keep honest.
- DISPLAY: EpDisplay gains Unbound (picker/model.rs — currently Offline-gray/Online-green/HarnessOnly-amber/Controlled-blue). amber is TAKEN (HarnessOnly=not-controllable, semantic opposite of Unbound which IS attachable). Use HOLLOW treatment (operator: "hollow is good precedent") + a CONTROLLED-VARIANT of hollow (Unbound + driven_by).

STILL TO GRILL (paused): skeleton/Unbound lifecycle cleanup (teardown if never binds — now visible via UNBOUND so operator can act; session-death→OFFLINE; purge); MRU storage/scope; ADR (skeleton-with-home creation flow + STATUS_UNBOUND + attach-on-session — likely warrants one ADR). Then mint REQ-A + REQ-B, dispatch todlando after v0.13.2. perri committing repro script (regression test MUST run multi-subnet).


Updated task #8 description


Grill captured in task #8. Now the careful cross-process gate of W3d-ii (the keystone). Fetch + diff:


Bash

git pull --ff-only --quiet 2>&1 | tail -1; echo "===HEAD==="; git log --oneline -1 | cat; echo "===W3d-ii diff stat b62476e..322ca55==="; git diff --stat b62476e..322ca55 | cat

Fetch W3d-ii + diff stat


===HEAD===
322ca55 feat(daemon): W3d-ii broker-coordinated live adapter apply (ADR-0025)
===W3d-ii diff stat b62476e..322ca55===
 crates/spt-daemon/src/brain.rs  |  34 +++++-
 crates/spt-daemon/src/broker.rs | 259 ++++++++++++++++++++++++++++++++++------
 crates/spt-daemon/src/msg.rs    |  22 ++++
 crates/spt/src/cli.rs           | 150 ++++++++++++++++++++++-
 4 files changed, 425 insertions(+), 40 deletions(-)


git diff b62476e..322ca55 -- crates/spt-daemon/src/broker.rs | grep -v "^+.*test\|^+ *//[^!]" | sed -n '1,200p'

Read broker dispatch_adapter_apply + build_translation factor