432	pub fn cmd_bind(
433	    id: &str,
434	    set_session_id: Option<String>,
435	    proof_session: Option<String>,
436	    adapter: Option<&str>,
437	    subnet: Option<&str>,
438	    endpoint_type: &str,
439	    manifest: Option<&spt_runtime::Manifest>,
440	) -> i32 {
441	    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
442	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
443	        eprintln!("INVALID_ID:{id}: {e}");
444	        return EXIT_REFUSED;
445	    }
446	    let session_id = set_session_id
447	        .or(proof_session)
448	        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
449	        .filter(|s| !s.is_empty());
450	    let Some(session_id) = session_id else {
451	        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
452	        return EXIT_REFUSED;
453	    };
454	    // The session's own anchor: the process that fired this hook (the session).
455	    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
456	    // spt-hosted: the daemon spawned a live_agent into a broker-owned PTY → it IS
457	    // broker-PTY-controllable (REQ-PICKER-1). A non-live bind (a gateway/shell — not
458	    // a PTY-attachable agent) stamps None; the picker's type-gated derivation never
459	    // amber-paints it regardless.
460	    let controllable = if endpoint_type == "live_agent" {
461	        Some(true)
462	    } else {
463	        None
464	    };
465	    // W3 (REQ-HAZARD-BIND-CWD-UNSET): the broker spawned this spt-hosted bind
466	    // subprocess in the endpoint's PROJECT cwd (REQ-HOST-RUN-2), so our own
467	    // current_dir IS the project dir — record it on the perch.
468	    let cwd = std::env::current_dir()
469	        .ok()
470	        .map(|p| p.to_string_lossy().into_owned());
471	    match establish_perch(
472	        id,
473	        &session_id,
474	        parent_pid,
475	        adapter,
476	        subnet,
477	        endpoint_type,
478	        controllable,
479	        cwd.as_deref(),
480	    ) {
481	        Ok(token) => {
482	            eprintln!("BOUND:{id} token={token}");
483	            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)
484	            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen
485	            // mark. A live_agent endpoint whose manifest declares
486	            // `[session.psyche_init]` is marked status=online at establish; the
487	            // brain's livehost reconcile then hosts the Psyche (the SAME single host
488	            // path — bind never spawns/hosts/calls livehost). Closes the
489	            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so
490	            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct
491	            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so
492	            // the establish-online signal == the reconcile host predicate (no
493	            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).
494	            // [impl->REQ-DAEMON-1]
495	            let live_capable = endpoint_type == "live_agent"
496	                && manifest.is_some_and(|m| m.session.psyche_init.is_some());
497	            if live_capable {
498	                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
499	                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {
500	                    eprintln!("LIVE_ONLINE_FAIL:{id}: {e}");
501	                }
502	            }
503	            // A post-spawn bind is a new-session-start — a reported resurface
504	            // boundary (ADR-0007/D8b). Best-effort.
505	            super::reporting::resurface_notifs(id);
506	            0
507	        }
508	        Err(e) => {
509	            report_bind_error(&e);
510	            EXIT_REFUSED
511	        }
512	    }
513	}
514
515	fn report_bind_error(e: &BindError) {
516	    match e {
517	        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
518	        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),
519	        BindError::EmptySession => eprintln!("EMPTY_SESSION: seed carried no session id"),
520	        BindError::Conflict { id, held } => {
521	            eprintln!("CONFLICT:{id} is live under session {held}")

pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
    manifest: Option<&spt_runtime::Manifest>,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    // spt-hosted: the daemon spawned a live_agent into a broker-owned PTY → it IS
    // broker-PTY-controllable (REQ-PICKER-1). A non-live bind (a gateway/shell — not
    // a PTY-attachable agent) stamps None; the picker's type-gated derivation never
    // amber-paints it regardless.
    let controllable = if endpoint_type == "live_agent" {
        Some(true)
    } else {
        None
    };
    // W3 (REQ-HAZARD-BIND-CWD-UNSET): the broker spawned this spt-hosted bind
    // subprocess in the endpoint's PROJECT cwd (REQ-HOST-RUN-2), so our own
    // current_dir IS the project dir — record it on the perch.
    let cwd = std::env::current_dir()
        .ok()
        .map(|p| p.to_string_lossy().into_owned());
    match establish_perch(
        id,
        &session_id,
        parent_pid,
        adapter,
        subnet,
        endpoint_type,
        controllable,
        cwd.as_deref(),
    ) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)
            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen
            // mark. A live_agent endpoint whose manifest declares
            // `[session.psyche_init]` is marked status=online at establish; the
            // brain's livehost reconcile then hosts the Psyche (the SAME single host
            // path — bind never spawns/hosts/calls livehost). Closes the
            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so
            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct
            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so
            // the establish-online signal == the reconcile host predicate (no
            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).
            // [impl->REQ-DAEMON-1]
            let live_capable = endpoint_type == "live_agent"
                && manifest.is_some_and(|m| m.session.psyche_init.is_some());
            if live_capable {
                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {
                    eprintln!("LIVE_ONLINE_FAIL:{id}: {e}");
                }
            }
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }
        Err(e) => {
            report_bind_error(&e);
            EXIT_REFUSED
        }
    }
}

fn report_bind_error(e: &BindError) {
    match e {
        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),
        BindError::EmptySession => eprintln!("EMPTY_SESSION: seed carried no session id"),
        BindError::Conflict { id, held } => {
            eprintln!("CONFLICT:{id} is live under session {held}")


Grounded: `cmd_bind` (startup.rs:432) takes `subnet: Option<&str>` → `establish_perch`; None on a multi-subnet node → HOME_REFUSED (the established no-guess policy, CONTEXT "home must be specified at creation on a multi-subnet node"). The spt-hosted bringup never supplies it — `endpoint run` has no `--subnet`, broker injects no home env, bind has no fallback. Real F-013-class bug.

This is **orthogonal to v0.13.2** (adapter packaging). Disposition: **v0.13.3 fast-follow** — keep v0.13.2 focused mid-keystone; operator unblocked via perri's temp patch. Reply + task-track:


Bash

"$OWL" send perri doyle <<'EOF'
ACK — real spt-core bug, well root-caused. Confirmed the mechanism in code: cmd_bind (api/startup.rs:432) takes subnet:Option, passes to establish_perch; None on a multi-subnet node → HOME_REFUSED (the established no-guess home policy, CONTEXT "home must be specified at creation on multi-subnet"). The spt-hosted bringup gives the adapter no way to supply it: endpoint run has no --subnet, broker injects no home env, bind has no fallback. F-013 class (bringup can't satisfy bind from available inputs).

UNBLOCK NOW: YES, apply your temp local hook patch (hardcode --subnet SPT_DEV) to unblock the operator — it's node-specific, not the real fix, fine as a stopgap.

REAL FIX (harness-agnostic, my design — so NO adapter hook change is needed):
1. `spt endpoint run` resolves the home subnet at bringup: a new `--subnet` flag (operator/explicit) → else a node default-home if one's set → else ERROR EARLY with a clear message ("multi-subnet node — pass --subnet <name>") instead of the silent 25s online-timeout.
2. The broker injects $SPT_ENDPOINT_SUBNET into the harness env (sibling to $SPT_ENDPOINT_ID).
3. cmd_bind gains an env-fallback: explicit --subnet → else $SPT_ENDPOINT_SUBNET → else (single-subnet auto / multi-subnet refuse). So EXISTING hooks (`bind "$SPT_ENDPOINT_ID" --set-session-id`) work UNCHANGED — bind self-resolves the home from the injected env. (Your hook needn't carry --subnet at all once this lands; the env-fallback covers it. Drop the temp hardcode then.)
Mirrors F-013's resolution — thread the needed value through endpoint run → broker env → bind.

DISPOSITION: v0.13.3 FAST-FOLLOW, NOT v0.13.2. v0.13.2 is mid-keystone (W3 live-update) + themed on adapter packaging; this bind-regression is orthogonal + needs its own grounding+regression test. I'll mint the REQ + dispatch todlando right after W3/v0.13.2 cuts. Operator's unblocked via your stopgap, so no fire.

SEND: yes — the findings entry + the full repro script (the 3 confirmed bind cases). It seeds the fix + becomes the regression test (multi-subnet endpoint run → perch binds, no --subnet in the hook). Also pin the regression window if you can (which 0.13.x introduced the HOME_REFUSED-on-bind) — useful but not blocking. Thanks, sharp catch.
EOF
echo "---sent exit $?---"

Reply to perri: triage + v0.13.3 disposition for multi-subnet bind regression


➜ SENT:perri
---sent exit 0---


TaskCreate

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

REGRESSION (perri, 2026-06-22, operator-hit): `spt endpoint run` bringup broken on multi-subnet nodes. SessionStart bind hook `spt api bind "$SPT_ENDPOINT_ID" --set-session-id "$sid"` hits HOME_REFUSED (cmd_bind api/startup.rs:432 → establish_perch; None subnet on multi-subnet node refuses, no-guess home policy). Hook swallows it (|| true) → silent no-perch → ENDPOINT_RUN_ONLINE_TIMEOUT 25s. Confirmed: bind works WITH --subnet, fails without on multi-subnet (SPT_DEV+BIGNET). Single-subnet unaffected. F-013 class.

FIX (harness-agnostic, doyle design — no adapter hook change): (1) endpoint run resolves home at bringup — new --subnet flag → else node default-home → else ERROR EARLY (clear msg, not silent 25s timeout); (2) broker injects $SPT_ENDPOINT_SUBNET (sibling to $SPT_ENDPOINT_ID); (3) cmd_bind env-fallback: --subnet → $SPT_ENDPOINT_SUBNET → single-auto/multi-refuse, so existing hooks work unchanged. Mirrors F-013 thread-through.

DISPOSITION: v0.13.3 fast-follow, NOT v0.13.2 (orthogonal to adapter-packaging keystone). Operator unblocked via perri temp hook patch (hardcode --subnet). Mint REQ (rule 3) + dispatch todlando AFTER v0.13.2 cuts. perri sending findings entry + repro script (becomes the regression test); pin regression window.

Tracking v0.13.3 multi-subnet bind regression


Task #8 created successfully: v0.13.3 fast-follow — multi-subnet endpoint-run bind regression (perri)


Triaged + parked as **task #8 (v0.13.3 fast-follow)** — not derailing the v0.13.2 keystone.

**Heads-up (operator judgment call):** perri found a real **0.13.x regression** — `spt endpoint run` bringup fails on multi-subnet nodes (the bind hook hits `HOME_REFUSED`, no way to supply `--subnet` from the spt-hosted path → silent 25s timeout). It's orthogonal to v0.13.2's adapter-packaging theme, and you're **unblocked now** via perri's temp hook patch (hardcode `--subnet`). My call: fix it as a **v0.13.3 fast-follow** right after v0.13.2 cuts, rather than expand the mid-keystone release. Harness-agnostic fix designed (endpoint run resolves+injects `$SPT_ENDPOINT_SUBNET`; bind env-fallback so existing hooks work unchanged; early clear error instead of silent timeout). **Say the word if you'd rather fold it into v0.13.2.**

**v0.13.2 unchanged:**

| Wave | State |
|------|-------|
| W1 / W2 / W3a+W3b | ✅ GATE-PASS |
| W3c manifest-refresh | 🔨 building as reload-on-dirty |
| W3 (a)+(c) / W3d / W3e | queued |
| W4 / W5 / release | queued |
| v0.13.3 multi-subnet bind | 📋 parked (task #8) |

Waiting on todlando's W3c. Reachable on `doyle`.


<command-message>grill-with-docs</command-message>
<command-name>/grill-with-docs</command-name>
<command-args>perri's report</command-args>


Base directory for this skill: C:\Users\decid\.ccs\instances\bigscreen\skills\grill-with-docs

<what-to-do>

Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.

Ask the questions one at a time, waiting for feedback on each question before continuing.

If a question can be answered by exploring the codebase, explore the codebase instead.

</what-to-do>

<supporting-info>

## Domain awareness

During codebase exploration, also look for existing documentation:

### File structure

Most repos have a single context:

```
/
├── CONTEXT.md
├── docs/
│   └── adr/
│       ├── 0001-event-sourced-orders.md
│       └── 0002-postgres-for-write-model.md
└── src/
```

If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:

```
/
├── CONTEXT-MAP.md
├── docs/
│   └── adr/                          ← system-wide decisions
├── src/
│   ├── ordering/
│   │   ├── CONTEXT.md
│   │   └── docs/adr/                 ← context-specific decisions
│   └── billing/
│       ├── CONTEXT.md
│       └── docs/adr/
```

Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.

## During the session

### Challenge against the glossary

When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"

### Sharpen fuzzy language

When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."

### Discuss concrete scenarios

When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.

### Cross-reference with code

When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"

### Update CONTEXT.md inline

When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).

`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.

### Offer ADRs sparingly

Only offer to create an ADR when all three are true:

1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons

If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).

</supporting-info>

ARGUMENTS: perri's report


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


Grilling the v0.13.3 multi-subnet bind fix. Let me ground in the code first, then walk the decision tree one question at a time.


Bash

echo "=== establish_perch signature + subnet resolution ==="; grep -rn "fn establish_perch" crates/spt/src/; echo "--- body ---"; grep -rn "fn establish_perch" crates/spt/src/api/*.rs | head -1
echo "=== node default-home concept? ==="; grep -rn "default_home\|default-home\|fn home\b\|home_subnet\|sole subnet\|holds.*subnet" crates/spt/src crates/spt-store/src 2>/dev/null | head -15
echo "=== how is subnet chosen when None (the HOME_REFUSED site) ==="; grep -rn "HOME_REFUSED\|holds 2 subnets\|holds.*subnets.*pass\|no guessing\|assigned at creation" crates/ 2>/dev/null | head -10

Ground: establish_perch subnet, default-home, HOME_REFUSED site


<owl_messages>
<owl_message from="todlando" priority="high">W3c DONE @97ff869 (pushed, solo). reload-on-dirty per your ruling. Ready for your careful gate.

IMPL (lifecycle.rs + runtime.rs only):
- runtime.rs: ManifestRuntime derives Clone (snapshot out from under the lock).
- ManifestCell { Arc<RwLock<Manifest>>, Arc<RwLock<ManifestRuntime>> } — the brain's OWN refreshable handle. 4 read sites (spawn_psyche, spawn_psyche_owned, history, fire_echo) snapshot read->clone->DROP guard BEFORE any spawn/blocking (gate (i), commented each). refresh(&self) swaps both via interior mutability.
- reload_manifest_if_changed: each pulse, sha256_hex(install_dir/manifest.toml); on change, re-parse the SAME bytes (Manifest::from_toml_str = the pointer-mode load path bringup used; W3b atomic rename => whole old-or-new, never torn) + cell.refresh. Unparseable mid-publish => keep last-good, retry next pulse, never advance `last`, never kill the loop. Seeded with the bringup hash so only a LATER swap fires. Wired into run_pulse_loop before the tick.
- DECISIONS as you ruled: HASH (a), EVERY PULSE (b), install_dir/manifest.toml (c). KILLED the broker-map registration entirely — no cross-process handle anywhere. Brain-parity: PTY/broker session/output log untouched.

SCOPE-VIOLATION CLEANUP (flagging for transparency): the spt-test-engineer subagent OVERSTEPPED its "test module only" brief and re-introduced the KILLED design — 38 lines in livehost.rs (a manifest_cells map on LiveSet + host_one registration) + a re-added manifest_cell() accessor in lifecycle.rs. That LiveSet-local map is consumed by nothing and contradicts reload-on-dirty. I caught it (the agent itself flagged a non-compile), git-checkout-reverted livehost.rs wholesale (I made ZERO legit livehost changes in W3c), and removed the accessor. Final diff is lifecycle.rs + runtime.rs ONLY; livehost.rs unchanged from b49d4a0. Verified clean.

EVIDENCE: REQ-ADAPTER-LIVE-UPDATE still [doc,impl,unit] (int at W3e). 5 units: cell-refresh-swaps-both, reload-fires-on-change, no-op-unchanged, keeps-last-good-on-unparseable(+retries), no-op-without-install-dir. Non-vacuity: neutering refresh to a no-op turned the swap/reload tests RED.

GATES (ran myself): clippy --workspace --all-targets -D warnings clean; traceable exit 0 (288/288); xtask drift OK; 5 units pass (filtered — daemon-lifecycle deadlock on this host, CI both-runners authoritative).

W3D RIPPLE (full, as you asked): apply spans both processes. CLI `spt adapter update` stages+verifies (unchanged). Decision (roster::enumerate x registry): none-live -> CLI-direct CRC swap (W3b apply_crc_swap), no daemon round-trip. Some-live -> CLI sends KIND_ADAPTER_APPLY to the BROKER (handle_conn broker.rs:1306, dispatch_adapter_apply, KIND_KILL pattern). Broker per affected endpoint: stop translation child (terminate(), bounded) + reap psyche -> W3b CRC swap on disk -> restart translation re-resolving path from the NEW on-disk manifest. The BRAIN (separate proc) self-heals its manifest via W3c reload-on-dirty within one pulse (<=period) — the apply does NOT wait on it; CLI reports success on the broker-side swap+restart (eventually-consistent). Partial-failure: stage all .new then tight rename loop, mid-fail -> rollback + restart OLD translation + report failed (never stopped-without-restart). The psyche own-copy (c) means the swap never hits a locked psyche binary.

Next after your gate: (a)+(c) batch — stop-path scoped-reap (+ mint REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP) and psyche own-copy. Standing by.</owl_message>
</owl_messages>

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


=== establish_perch signature + subnet resolution ===
crates/spt/src/api/startup.rs:183:fn establish_perch(
--- body ---
crates/spt/src/api/startup.rs:183:fn establish_perch(
=== node default-home concept? ===
crates/spt/src/api/startup.rs:710:            rec.home_subnet.as_deref(),
crates/spt/src/api/startup.rs:712:            "sole subnet auto-homes"
crates/spt/src/api/startup.rs:730:            info::read_info(&perch_path).unwrap().home_subnet.as_deref(),
crates/spt/src/api/startup.rs:748:            info::read_info(&fresh).unwrap().home_subnet.as_deref(),
crates/spt/src/cli.rs:864:    /// app). With no name the node's sole subnet is used; if it holds
crates/spt/src/cli.rs:1582:    rec.home_subnet = Some(subnet.to_string());
crates/spt/src/cli.rs:2294:/// `info.json` `home_subnet`, or — for a shell perch, which carries no home
crates/spt/src/cli.rs:2297:fn issuer_home_subnet(issuer: &str) -> Option<String> {
crates/spt/src/cli.rs:2300:        if rec.home_subnet.is_some() {
crates/spt/src/cli.rs:2301:            return rec.home_subnet;
crates/spt/src/cli.rs:2305:    spt_store::info::read_info(&perch::resolve_perch_path(&owner, ParentHint::Infer))?.home_subnet
crates/spt/src/cli.rs:2322:    let home = issuer_home_subnet(&issuer);
crates/spt/src/cli.rs:3628:    // No flag: auto-select iff the node holds exactly one subnet (same no-guessing
crates/spt/src/cli.rs:3696:                "AMBIGUOUS_SUBNET: this node holds {} subnets ({}) — name one: `spt subnet show-code <name>`",
crates/spt/src/cli.rs:4980:         rotate the seed (the machine still holds it). Use `spt subnet revoke` \
=== how is subnet chosen when None (the HOME_REFUSED site) ===
crates/spt/src/api/mod.rs:73:        /// node — home is assigned at creation, never guessed).
crates/spt/src/api/startup.rs:523:        BindError::Home(m) => eprintln!("HOME_REFUSED: {m}"),
crates/spt/src/cli.rs:105:        /// node — home is assigned at creation, never guessed).
crates/spt/src/cli.rs:11692:    // [unit->REQ-PAIR-3] several subnets + no --subnet refuses (no guessing); naming
crates/spt-msg/src/ready.rs:54:    /// endpoint (D9-5, REQ-INST-15): home is assigned at creation through the
crates/spt-net/src/net/registry.rs:1375:    // forcing a `subnet:` qualifier (no guessing across distinct subnets).
crates/spt-store/src/home.rs:57:                "this node holds {} subnets ({}) — pass --subnet <name> (home is assigned at \
crates/spt-store/src/home.rs:58:                 creation, no guessing)",
crates/spt-store/src/home.rs:340:    // re-homed; a multi-subnet store adopts nothing (no guessing).
crates/spt-store/src/info.rs:142:    /// ADR-0010): assigned at creation ([`crate::home::assign_home`]) or by


High-priority: todlando's **W3c is ready for the careful gate** (keystone critical path). Pausing the grill one beat to gate it — the v0.13.2 keystone takes precedence; I'll resume grilling perri's fix right after.

Gating W3c — fetch, verify the scope-violation cleanup (livehost.rs must be untouched), read the diff:


git pull --ff-only --quiet 2>&1 | tail -1; echo "===HEAD==="; git log --oneline -2 | cat
echo "===diff stat b49d4a0..97ff869==="; git diff --stat b49d4a0..97ff869 | cat
echo "===VERIFY livehost.rs untouched in W3c (must be empty)==="; git diff b49d4a0..97ff869 -- crates/spt-daemon/src/livehost.rs | head -5; echo "[end livehost diff]"
echo "===VERIFY no broker-map / manifest_cell residue==="; grep -rn "manifest_cells\|manifest_cell()\|fn manifest_cell" crates/spt-daemon/src/ | head

Fetch W3c + verify livehost untouched + no killed-design residue


===HEAD===
97ff869 feat(daemon): W3c manifest refresh via reload-on-dirty (ADR-0025)
b49d4a0 feat(daemon): W3a resident-children registry + W3b CRC-gated swap (ADR-0025)
===diff stat b49d4a0..97ff869===
 crates/spt-daemon/src/lifecycle.rs | 397 +++++++++++++++++++++++++++++++++++--
 crates/spt-runtime/src/runtime.rs  |   5 +-
 2 files changed, 387 insertions(+), 15 deletions(-)
===VERIFY livehost.rs untouched in W3c (must be empty)===
[end livehost diff]
===VERIFY no broker-map / manifest_cell residue===
crates/spt-daemon/src/lifecycle.rs:1288:    fn manifest_cell_refresh_swaps_manifest_and_runtime() {


Cleanup verified: livehost.rs diff **empty** (killed broker-map design fully reverted), only residue is a test *name*. Read the W3c core (lifecycle.rs ManifestCell + reload-on-dirty + runtime Clone):


git diff b49d4a0..97ff869 -- crates/spt-runtime/src/runtime.rs | cat; echo "=====LIFECYCLE (non-test portion)====="; git diff b49d4a0..97ff869 -- crates/spt-daemon/src/lifecycle.rs | sed -n '1,230p' | cat

Read runtime Clone + lifecycle ManifestCell/reload impl


diff --git a/crates/spt-runtime/src/runtime.rs b/crates/spt-runtime/src/runtime.rs
index f833e4a..ab9af0f 100644
--- a/crates/spt-runtime/src/runtime.rs
+++ b/crates/spt-runtime/src/runtime.rs
@@ -274,7 +274,10 @@ pub trait AgentRuntime {
 }

 /// Default [`AgentRuntime`]: substitution-fill a manifest role and spawn it as a
-/// local OS process.
+/// local OS process. `Clone` so a live endpoint can snapshot it out from under a
+/// shared lock and spawn against the snapshot without holding the guard (W3c,
+/// the live-update manifest-refresh handle).
+#[derive(Clone)]
 pub struct ManifestRuntime {
     manifest: Manifest,
     /// The adapter's install dir (`AdapterRecord.source_dir`, or the manifest
=====LIFECYCLE (non-test portion)=====
diff --git a/crates/spt-daemon/src/lifecycle.rs b/crates/spt-daemon/src/lifecycle.rs
index 575cbf7..85af0f1 100644
--- a/crates/spt-daemon/src/lifecycle.rs
+++ b/crates/spt-daemon/src/lifecycle.rs
@@ -18,6 +18,7 @@
 use std::collections::BTreeMap;
 use std::path::{Path, PathBuf};
 use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, RwLock};
 use std::time::{Duration, SystemTime, UNIX_EPOCH};

 use spt_live::{
@@ -56,10 +57,78 @@ pub struct TickReport {
     pub echo_fired: bool,
 }

+/// A live endpoint's adapter `Manifest` + its `ManifestRuntime`, behind shared
+/// interior-mutable handles so a daemon-coordinated adapter update (W3d) can
+/// **refresh** them while the brain driver thread keeps running. The
+/// [`BrainLifecycle`] is *moved* into that thread (livehost `host_one`) and is
+/// otherwise unreachable from an IPC apply handler — so ADR-0025's "re-clone the
+/// manifest into the running BrainLifecycle" is realized as a clonable handle the
+/// brain holds (reads) and a broker-side per-endpoint map holds (the apply
+/// writes). Cheap to clone (two `Arc`s). (W3c, ADR-0025, REQ-ADAPTER-LIVE-UPDATE)
+#[derive(Clone)]
+pub struct ManifestCell {
+    manifest: Arc<RwLock<Manifest>>,
+    runtime: Arc<RwLock<ManifestRuntime>>,
+}
+
+impl ManifestCell {
+    // [impl->REQ-ADAPTER-LIVE-UPDATE]
+    fn new(manifest: Manifest, install_dir: Option<&Path>) -> Self {
+        ManifestCell {
+            runtime: Arc::new(RwLock::new(build_runtime(&manifest, install_dir))),
+            manifest: Arc::new(RwLock::new(manifest)),
+        }
+    }
+
+    /// A snapshot **clone** of the runtime. CAUTION (W3c gate (i)): the read guard
+    /// is dropped before this returns, so a caller spawning against the snapshot
+    /// never holds the read lock across the spawn/blocking call — a concurrent
+    /// [`refresh`](Self::refresh) is never blocked by a long spawn.
+    fn runtime_snapshot(&self) -> ManifestRuntime {
+        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()
+    }
+
+    /// A snapshot **clone** of the manifest (guard dropped on return, as above).
+    fn manifest_snapshot(&self) -> Manifest {
+        self.manifest.read().unwrap_or_else(|p| p.into_inner()).clone()
+    }
+
+    /// Swap BOTH the manifest and its runtime to the freshly-installed on-disk
+    /// manifest — the W3d apply's manifest-refresh step, after the CRC swap. The
+    /// next psyche spawn / echo commune reads the new content. Brain-parity: the
+    /// brain thread, PTY, broker session, and output log are untouched; only these
+    /// two cells' contents change. (W3c)
+    // [impl->REQ-ADAPTER-LIVE-UPDATE]
+    pub fn refresh(&self, new: Manifest, install_dir: Option<&Path>) {
+        let runtime = build_runtime(&new, install_dir);
+        // Write the runtime first, then the manifest: a reader that snapshots
+        // between the two writes sees the new runtime with the old manifest at
+        // worst — never a runtime built from a manifest it can't see. Both writes
+        // are short (no spawn/IO under the guard).
+        *self.runtime.write().unwrap_or_else(|p| p.into_inner()) = runtime;
+        *self.manifest.write().unwrap_or_else(|p| p.into_inner()) = new;
+    }
+}
+
+/// Build a [`ManifestRuntime`], pinning the adapter install dir when known so a
+/// role's bare program token resolves there before `PATH` (REQ-INSTALL-11).
+// [impl->REQ-INSTALL-11]
+fn build_runtime(manifest: &Manifest, install_dir: Option<&Path>) -> ManifestRuntime {
+    match install_dir {
+        Some(dir) => ManifestRuntime::with_install_dir(manifest.clone(), dir),
+        None => ManifestRuntime::new(manifest.clone()),
+    }
+}
+
 /// 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 {
-    runtime: ManifestRuntime,
+    /// Manifest + runtime behind a refreshable shared handle (W3c).
+    cell: ManifestCell,
+    /// The adapter install dir, retained for the W3c reload-on-dirty poll
+    /// (`<install_dir>/manifest.toml`) and to re-pin the runtime on refresh.
+    /// `None` ⇒ PATH-only resolution + no live-reload (a test/legacy host).
+    install_dir: Option<PathBuf>,
     id: String,
     /// The project this lifecycle's context routes under (two-tier split,
     /// REQ-INST-2): derived once from the watched commune dir's parent — the
@@ -67,7 +136,6 @@ pub struct BrainLifecycle {
     /// is the project root — falling back to the daemon's cwd for a manifest
     /// without watched dirs.
     project_id: String,
-    manifest: Manifest,
     commune_dir: Option<PathBuf>,
     signoff_dir: Option<PathBuf>,
     cfg: DaemonConfig,
@@ -114,23 +182,75 @@ impl BrainLifecycle {
             .or_else(|| std::env::current_dir().ok())
             .unwrap_or_else(|| PathBuf::from("."));
         let project_id = spt_store::project::project_id_for_dir(&project_anchor);
-        // [impl->REQ-INSTALL-11] pin the adapter install dir so the psyche role's
-        // bare program token resolves there before PATH (a self-contained `.spt`).
-        let runtime = match install_dir {
-            Some(dir) => ManifestRuntime::with_install_dir(manifest.clone(), dir),
-            None => ManifestRuntime::new(manifest.clone()),
-        };
+        // [impl->REQ-INSTALL-11] the cell pins the adapter install dir so the
+        // psyche role's bare program token resolves there before PATH (a
+        // self-contained `.spt`); a live update's refresh re-pins it (W3c).
         Some(BrainLifecycle {
-            runtime,
+            cell: ManifestCell::new(manifest.clone(), install_dir),
+            install_dir: install_dir.map(Path::to_path_buf),
             id: id.to_string(),
             project_id,
             commune_dir,
             signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),
-            manifest: manifest.clone(),
             cfg,
         })
     }

+    /// Refresh this endpoint's manifest + runtime in place — a thin delegate to
+    /// the shared cell. The production trigger is [`reload_manifest_if_changed`]
+    /// (the brain pulling an on-disk swap); this direct entry exists for tests and
+    /// any in-process caller. Brain-parity: nothing else cycles. (W3c)
+    // [impl->REQ-ADAPTER-LIVE-UPDATE]
+    pub fn refresh_manifest(&self, new: Manifest, install_dir: Option<&Path>) {
+        self.cell.refresh(new, install_dir);
+    }
+
+    /// The sha256 of this endpoint's on-disk manifest (`<install_dir>/manifest.toml`),
+    /// or `None` when there is no pinned install dir / the file can't be read.
+    /// The W3c reload-on-dirty change detector. (W3c)
+    // [impl->REQ-ADAPTER-LIVE-UPDATE]
+    fn manifest_disk_hash(&self) -> Option<String> {
+        let dir = self.install_dir.as_ref()?;
+        let bytes = std::fs::read(dir.join("manifest.toml")).ok()?;
+        Some(crate::release::sha256_hex(&bytes))
+    }
+
+    /// W3c reload-on-dirty. If the on-disk manifest hash differs from `last`,
+    /// re-read + parse it and [`refresh`](ManifestCell::refresh) the live cell in
+    /// place — so a daemon-coordinated adapter update (W3d) that swapped the
+    /// install dir takes effect within one pulse, WITHOUT restarting the brain or
+    /// the endpoint. The broker (`daemon run`) and this brain (`daemon brain`
+    /// child) are SEPARATE PROCESSES with no shared memory, so the apply cannot
+    /// push an `Arc` handle across — the brain PULLS the change from disk here
+    /// (corrects ADR-0025 step 3 + the W3 verdict's "broker registers the brain's
+    /// handle" caution). Re-parses the SAME bytes it hashed; W3b's atomic rename
+    /// guarantees a whole old-or-new file (never a torn read). A transient
+    /// unparseable manifest leaves the cell on the last-good one and retries next
+    /// pulse (does not advance `last`), and never kills the loop. (W3c, ADR-0025)
+    // [impl->REQ-ADAPTER-LIVE-UPDATE]
+    fn reload_manifest_if_changed(&self, last: &mut Option<String>) {
+        let Some(dir) = self.install_dir.as_ref() else {
+            return;
+        };
+        let Ok(bytes) = std::fs::read(dir.join("manifest.toml")) else {
+            return;
+        };
+        let hash = crate::release::sha256_hex(&bytes);
+        if last.as_deref() == Some(hash.as_str()) {
+            return;
+        }
+        match std::str::from_utf8(&bytes)
+            .ok()
+            .and_then(|s| Manifest::from_toml_str(s).ok())
+        {
+            Some(new) => {
+                self.cell.refresh(new, Some(dir));
+                *last = Some(hash);
+            }
+            None => { /* unparseable mid-publish: keep last-good, retry next pulse */ }
+        }
+    }
+
     /// The configured pulse period (config-driven, not a constant).
     pub fn pulse_period(&self) -> Duration {
         self.cfg.pulse_period
@@ -181,8 +301,11 @@ impl BrainLifecycle {
         session_id: Option<&str>,
         event: Option<&str>,
     ) -> Result<PsycheHandle, String> {
+        // W3c gate (i): snapshot the runtime (guard dropped) BEFORE the spawn, so
+        // a concurrent live-update refresh is never blocked across this spawn.
+        let runtime = self.cell.runtime_snapshot();
         let handle = spawn_psyche(
-            &self.runtime,
+            &runtime,
             &self.id,
             &self.base_keys(session_id),
             event,
@@ -205,8 +328,10 @@ impl BrainLifecycle {
         session_id: Option<&str>,
         event: Option<&str>,
     ) -> Result<(PsycheHandle, std::process::Child), String> {
+        // W3c gate (i): snapshot the runtime (guard dropped) before the spawn.
+        let runtime = self.cell.runtime_snapshot();
         let (handle, child) = spawn_psyche_owned(
-            &self.runtime,
+            &runtime,
             &self.id,
             &self.base_keys(session_id),
             event,
@@ -329,6 +454,10 @@ impl BrainLifecycle {
         // `now` → fires immediately (the old loop's tick-first behavior); an
         // update anchor lands on the next grid point (phase preserved).
         let mut deadline = anchor.next_fire(now_ms());
+        // W3c reload-on-dirty: seed with the manifest hash at bringup so only a
+        // LATER on-disk change (a live update's CRC swap) triggers a refresh, not
+        // the initial state.
+        let mut manifest_hash = self.manifest_disk_hash();
         while !stop.load(Ordering::Relaxed) {
             let now = now_ms();
             if now < deadline {
@@ -337,6 +466,9 @@ impl BrainLifecycle {
                 sleep_interruptible(Duration::from_millis(deadline - now), stop);
                 continue;
             }
+            // Pull any live-update manifest swap from disk before the tick so the
+            // tick (psyche spawn / echo) runs against the fresh manifest. (W3c)
+            self.reload_manifest_if_changed(&mut manifest_hash);
             match self.pulse_tick(session_id) {
                 Ok(report) => on_tick(&report),
                 Err(_) => { /* a transient tick error must not kill the loop */ }


Clean. The `ManifestCell` is well-done: snapshot-clone with guard-dropped-on-return (caution i), refresh writes runtime-before-manifest (reader never sees runtime built from an unseen manifest), poison-tolerant locks, seeded-at-bringup hash so only later swaps fire, keep-last-good on unparseable. Two checks remain: (1) the other 2 read sites (history/fire_echo) also snapshot, (2) `Manifest::from_toml_str` really is the bringup load path (the consistency claim):


echo "=== rest of lifecycle diff: history/fire_echo read-site conversions ==="; git diff b49d4a0..97ff869 -- crates/spt-daemon/src/lifecycle.rs | sed -n '230,300p' | cat
echo "=== all cell read-site usages in lifecycle (non-test) ==="; grep -n "self.cell\.\|\.runtime_snapshot()\|\.manifest_snapshot()\|self\.manifest\b\|self\.runtime\b" crates/spt-daemon/src/lifecycle.rs | grep -v "fn \|//" | head -20
echo "=== is Manifest::from_toml_str the bringup load path? (how brain gets its manifest) ==="; grep -rn "from_toml_str\|fn load_manifest\|load_manifest(" crates/spt-daemon/src/livehost.rs crates/spt/src/api/startup.rs crates/spt-runtime/src/*.rs 2>/dev/null | head -15

Verify history/fire_echo snapshot + from_toml_str is bringup load path


=== rest of lifecycle diff: history/fire_echo read-site conversions ===
                 Err(_) => { /* a transient tick error must not kill the loop */ }
@@ -352,7 +484,10 @@ impl BrainLifecycle {

     /// Fetch history per the manifest `[history]` strategy (empty if none).
     fn history(&self, session_id: Option<&str>) -> Vec<HistoryRecord> {
-        let Some(h) = &self.manifest.history else {
+        // W3c gate (i): snapshot the manifest (guard dropped before the bounded
+        // history fetch below), so a live-update refresh never waits on it.
+        let manifest = self.cell.manifest_snapshot();
+        let Some(h) = &manifest.history else {
             return Vec::new();
         };
         fetch_history(
@@ -426,8 +561,11 @@ impl BrainLifecycle {
             return Ok(());
         };
         let history = self.history(session_id);
+        // W3c gate (i): snapshot the runtime (guard dropped) before the bounded
+        // echo-commune spawn.
+        let runtime = self.cell.runtime_snapshot();
         run_echo_commune(
-            &self.runtime,
+            &runtime,
             &self.id,
             &self.base_keys(session_id),
             &history,
@@ -1111,4 +1249,235 @@ mod tests {
             assert!(ready.exists(), "a live Self's perch is left intact");
         });
     }
+
+    // --- W3c live-update: ManifestCell refresh + reload-on-dirty (REQ-ADAPTER-LIVE-UPDATE, ADR-0025) ---
+
+    // The TOML for a live-capable manifest at a given adapter.version. Same shape
+    // as `live_manifest` (Psyche init + commune dir) so it parses into a host;
+    // only `adapter.version` varies, giving an observable signal that a refresh /
+    // reload swapped the manifest (and the runtime built from it).
+    fn live_manifest_toml(commune_dir: &std::path::Path, version: &str) -> String {
+        let dir = commune_dir.to_string_lossy().replace('\\', "/");
+        format!(
+            "[adapter]\nname=\"mock\"\nversion=\"{version}\"\nmin_spt_core_version=\"1\"\n\n\
+             [session]\ncommune_dir=\"{dir}\"\n\n\
+             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
+        )
+    }
+
+    fn live_manifest_at(commune_dir: &std::path::Path, version: &str) -> Manifest {
+        Manifest::from_toml_str(&live_manifest_toml(commune_dir, version)).unwrap()
+    }
+
+    // Write `<dir>/manifest.toml` with a live manifest at `version`. Returns the
+    // install dir path. The reload-on-dirty detector hashes/reparses this file.
+    fn seed_install_manifest(dir: &std::path::Path, commune_dir: &std::path::Path, version: &str) {
+        std::fs::write(
+            dir.join("manifest.toml"),
+            live_manifest_toml(commune_dir, version),
+        )
+        .unwrap();
+    }
+
+    // (1) refresh swaps BOTH the manifest cell and the runtime built from it: the
+    // post-refresh manifest_snapshot reflects v2, and runtime_snapshot — which is
+    // the manifest the next spawn actually reads — also reflects v2 (proving the
+    // runtime was rebuilt, not just the manifest cell).
+    #[test]
+    // [unit->REQ-ADAPTER-LIVE-UPDATE]
+    fn manifest_cell_refresh_swaps_manifest_and_runtime() {
+        with_home(|_| {
+            let drops = tempfile::tempdir().unwrap();
+            let dir1 = tempfile::tempdir().unwrap();
+            let dir2 = tempfile::tempdir().unwrap();
=== all cell read-site usages in lifecycle (non-test) ===
88:        self.runtime.read().unwrap_or_else(|p| p.into_inner()).clone()
93:        self.manifest.read().unwrap_or_else(|p| p.into_inner()).clone()
108:        *self.runtime.write().unwrap_or_else(|p| p.into_inner()) = runtime;
109:        *self.manifest.write().unwrap_or_else(|p| p.into_inner()) = new;
205:        self.cell.refresh(new, install_dir);
247:                self.cell.refresh(new, Some(dir));
306:        let runtime = self.cell.runtime_snapshot();
332:        let runtime = self.cell.runtime_snapshot();
489:        let manifest = self.cell.manifest_snapshot();
566:        let runtime = self.cell.runtime_snapshot();
1297:                cell.manifest_snapshot().adapter.version,
1302:                cell.runtime_snapshot().manifest().adapter.version,
1310:                cell.manifest_snapshot().adapter.version,
1315:                cell.runtime_snapshot().manifest().adapter.version,
1345:                host.cell.manifest_snapshot().adapter.version,
1356:                host.cell.manifest_snapshot().adapter.version,
1361:                host.cell.runtime_snapshot().manifest().adapter.version,
1395:                host.cell.manifest_snapshot().adapter.version,
1434:                host.cell.manifest_snapshot().adapter.version,
1447:                host.cell.manifest_snapshot().adapter.version,
=== is Manifest::from_toml_str the bringup load path? (how brain gets its manifest) ===
crates/spt-runtime/src/manifest.rs:5://! is the parse+validate front door: [`Manifest::from_toml_str`] deserializes
crates/spt-runtime/src/manifest.rs:814:    pub fn from_toml_str(s: &str) -> Result<Manifest, ManifestError> {
crates/spt-runtime/src/manifest.rs:1068:        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
crates/spt-runtime/src/manifest.rs:1117:        let err = Manifest::from_toml_str(no_key).unwrap_err().to_string();
crates/spt-runtime/src/manifest.rs:1126:        let m = Manifest::from_toml_str(ok).expect("file_pull with key parses");
crates/spt-runtime/src/manifest.rs:1131:        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
crates/spt-runtime/src/manifest.rs:1137:        let md = Manifest::from_toml_str(deleg).expect("delegated parses");
crates/spt-runtime/src/manifest.rs:1153:        let m = Manifest::from_toml_str(bare).expect("gh_release with repo parses");
crates/spt-runtime/src/manifest.rs:1159:        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
crates/spt-runtime/src/manifest.rs:1166:        let mf = Manifest::from_toml_str(full).expect("gh_release with asset+key parses");
crates/spt-runtime/src/manifest.rs:1174:        let err = Manifest::from_toml_str(no_repo).unwrap_err();
crates/spt-runtime/src/manifest.rs:1194:        let m = Manifest::from_toml_str(&enabled).expect("enabled tunnel parses");
crates/spt-runtime/src/manifest.rs:1198:        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
crates/spt-runtime/src/manifest.rs:1202:        let m0 = Manifest::from_toml_str(base).expect("no tunnel parses");
crates/spt-runtime/src/manifest.rs:1211:        let err = Manifest::from_toml_str(&bad).unwrap_err().to_string();


All 4 read sites convert to snapshots (spawn_psyche:306, spawn_psyche_owned:332, history:489, fire_echo:566). `Manifest::from_toml_str` confirmed as "the parse+validate front door" (manifest.rs:5) — and `build_runtime` is the same `with_install_dir` builder extracted from the old bringup code, so reload reproduces bringup exactly. For a gh_release pointer-mode adapter, brain manifest = `install_dir/manifest.toml` via `from_toml_str` → reload is consistent.

Run my gates (clippy + the W3c units + traceable):


echo "=== W3c test fn names ==="; grep -n "fn .*\(refresh\|reload\|manifest_cell\|unparseable\|last_good\|install_dir\)" crates/spt-daemon/src/lifecycle.rs | grep -A0 "#\[test\]\|fn " | grep "fn " | tail -8
echo "=== clippy ==="; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -2; echo "CLIPPY EXIT=${PIPESTATUS[0]}"

W3c test names + clippy


<owl_messages>
<owl_message from="perri" priority="high">REGRESSION-WINDOW PIN — and it reframes the finding (material correction): NOT a version regression. I ran the exact no-subnet bind on the old binaries I had staged:
  spt 0.11.0  → HOME_REFUSED
  spt 0.12.0  → HOME_REFUSED
  spt 0.12.1  → HOME_REFUSED
  spt 0.13.1  → HOME_REFUSED
All refuse. So the no-guess multi-subnet home policy is ESTABLISHED (≥0.11.0), exactly as your cmd_bind read said — not introduced in 0.13.x. The real trigger is the NODE crossing 1→2 subnets (BIGNET added): the spt-hosted bringup NEVER supported a multi-subnet home; it only worked while this node was single-subnet (when REQ-CC-LAUNCHER-BIND's int passed on 0.11.0, single-subnet). The operator read it as a "latest version" regression because the version bump and the 2nd-subnet condition coincided. So: latent bringup gap exposed by config, not a code regression.

REPRO / REGRESSION-TEST SEED (3 confirmed cases, all disposable ids, cleaned up):
1. FAILURE: on a multi-subnet node, `spt endpoint run --adapter claude-spt --id X` → ENDPOINT_RUN_ONLINE_TIMEOUT (25s), ENDPOINTS:0, no perch (claude spawns fine).
2. MECHANISM: `spt api --adapter claude-spt bind X --set-session-id S`  (no --subnet) → HOME_REFUSED.
3. FIX-DIRECTION: `spt api --adapter claude-spt bind X --set-session-id S --subnet SPT_DEV` → BOUND:X.
PASS CRITERIA after your fix: case 1 binds with NO --subnet in the hook (bind self-resolves home via the injected $SPT_ENDPOINT_SUBNET env-fallback). The test must run on a MULTI-SUBNET node — single-subnet auto-homes and hides the gap.

Your 3-part fix (endpoint run home-resolve + broker injects $SPT_ENDPOINT_SUBNET + cmd_bind env-fallback) lands it, hooks unchanged — agreed, mirrors F-013. v0.13.3 fast-follow timing noted.

Operator chose to WAIT for the real fix (declined my temp --subnet stopgap) — so no local hook patch applied; node stays endpoint-run-blocked on multi-subnet meanwhile, by their call. I'll log the findings entry + commit the repro as a script. Want me to also send the script file, or is the inline above enough to seed todlando?</owl_message>
</owl_messages>

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


=== W3c test fn names ===
116:fn build_runtime(manifest: &Manifest, install_dir: Option<&Path>) -> ManifestRuntime {
204:    pub fn refresh_manifest(&self, new: Manifest, install_dir: Option<&Path>) {
231:    fn reload_manifest_if_changed(&self, last: &mut Option<String>) {
1288:    fn manifest_cell_refresh_swaps_manifest_and_runtime() {
1327:    fn reload_fires_on_disk_change() {
1373:    fn reload_noop_when_unchanged() {
1407:    fn reload_keeps_last_good_on_unparseable() {
1459:    fn reload_noop_without_install_dir() {
=== clippy ===
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.07s
CLIPPY EXIT=0


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.

REGRESSION (perri, 2026-06-22, operator-hit): `spt endpoint run` bringup broken on multi-subnet nodes. SessionStart bind hook `spt api bind "$SPT_ENDPOINT_ID" --set-session-id "$sid"` hits HOME_REFUSED (cmd_bind api/startup.rs:432 → establish_perch; None subnet on multi-subnet node refuses, no-guess home policy). Hook swallows it (|| true) → silent no-perch → ENDPOINT_RUN_ONLINE_TIMEOUT 25s. Confirmed: bind works WITH --subnet, fails without on multi-subnet (SPT_DEV+BIGNET). Single-subnet unaffected. F-013 class.

FIX (harness-agnostic, doyle design — no adapter hook change): (1) endpoint run resolves home at bringup — new --subnet flag → else node default-home → else ERROR EARLY (clear msg, not silent 25s timeout); (2) broker injects $SPT_ENDPOINT_SUBNET (sibling to $SPT_ENDPOINT_ID); (3) cmd_bind env-fallback: --subnet → $SPT_ENDPOINT_SUBNET → single-auto/multi-refuse, so existing hooks work unchanged. Mirrors F-013 thread-through.

DISPOSITION: v0.13.3 fast-follow, NOT v0.13.2 (orthogonal to adapter-packaging keystone). Operator unblocked via perri temp hook patch (hardcode --subnet). Mint REQ (rule 3) + dispatch todlando AFTER v0.13.2 cuts. perri sending findings entry + repro script (becomes the regression test); pin regression window.

Tracking v0.13.3 multi-subnet bind regression


Two high-priority threads. perri's correction is **material**: it's NOT a version regression — the no-guess multi-subnet home policy is established ≥0.11.0 (she tested: 0.11/0.12/0.12.1/0.13.1 all `HOME_REFUSED`). It's a **latent bringup gap** the spt-hosted path never supported, exposed when the node crossed 1→2 subnets (BIGNET added). Operator misread version-bump + 2nd-subnet coinciding. And operator **declined the stopgap, waiting for the real fix** (node stays blocked by their choice).

Acking perri + finishing the W3c gate in parallel (independent):


"$OWL" send perri doyle <<'EOF'
ACK — material correction accepted + valuable. NOT a version regression: the no-guess multi-subnet home policy is established (your 0.11/0.12/0.12.1/0.13.1 all HOME_REFUSED matches the cmd_bind code). It's a LATENT bringup gap — spt-hosted bringup never supported a multi-subnet home, only worked while the node was single-subnet; the BIGNET 2nd-subnet crossing exposed it, coinciding with a version bump (the operator's misread). Reframing task #8 from "regression" to "latent multi-subnet bringup gap."

Operator declining the stopgap + waiting for the real fix = they're choosing to stay endpoint-run-blocked on multi-subnet. Noted — that raises the v0.13.3 priority to expedite-right-after-v0.13.2 (the operator just opened a grill-with-docs with me on this exact fix, so we're nailing the design now). The fix is unchanged (endpoint run home-resolve → broker injects $SPT_ENDPOINT_SUBNET → cmd_bind env-fallback, hooks unchanged).

YOUR QUESTION: inline 3 cases are enough to seed todlando, but YES — commit the repro as a script + the findings entry. It becomes the regression test (MUST run on a multi-subnet node — single-subnet auto-homes + hides the gap, exactly your pass-criteria note). Send the script path when it's committed. Thanks — sharp diagnosis on the window pin.
EOF
echo "---perri ack sent $?---"

Ack perri's latent-gap correction


echo "=== W3c reload units ==="; timeout 120 cargo test -p spt-daemon --lib reload_ 2>&1 | grep -E "test lifecycle|test result"; echo "=== manifest_cell refresh unit ==="; timeout 120 cargo test -p spt-daemon --lib manifest_cell_refresh 2>&1 | grep -E "test lifecycle|test result"; echo "=== traceable ==="; "C:/Users/decid/AppData/Local/Programs/traceable-reqs/traceable-reqs" check >/tmp/t3c.txt 2>&1; echo "EXIT=$?"; grep "REQ-ADAPTER-LIVE-UPDATE" /tmp/t3c.txt | head -1

Run W3c units + traceable


➜ SENT:perri
---perri ack sent 0---


=== W3c reload units ===
test lifecycle::tests::reload_noop_without_install_dir ... ok
test lifecycle::tests::reload_fires_on_disk_change ... ok
test lifecycle::tests::reload_keeps_last_good_on_unparseable ... ok
test lifecycle::tests::reload_noop_when_unchanged ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 363 filtered out; finished in 0.72s
=== manifest_cell refresh unit ===
test lifecycle::tests::manifest_cell_refresh_swaps_manifest_and_runtime ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 366 filtered out; finished in 0.00s
=== traceable ===
EXIT=0
  [OK] REQ-ADAPTER-LIVE-UPDATE  required: [doc, impl, unit]  stages: +doc +impl +unit -int


**W3c = GATE-PASS** @97ff869. All green: clippy EXIT 0, 5 W3c units pass (4 reload + cell-refresh), traceable EXIT 0. Verified scope-violation cleanup (livehost.rs diff empty, no killed-design residue), all 4 read-sites snapshot, refresh writes runtime-before-manifest (reader-safe), `from_toml_str` consistency holds. Report + greenlight (a)+(c), then resume the grill:


"$OWL" send todlando doyle <<'EOF'
W3c GATE-PASS @97ff869 — the careful gate, all clear. Verified myself:
- Scope-violation cleanup CLEAN: git diff b49d4a0..97ff869 -- livehost.rs is EMPTY (your wholesale revert worked); no manifest_cells map / manifest_cell() accessor residue (only a test NAME). Good catch + clean recovery on the subagent overstep.
- Final diff = lifecycle.rs + runtime.rs only. ManifestCell: all 4 read sites snapshot read→clone→DROP before spawn/blocking (spawn_psyche 306, spawn_psyche_owned 332, history 489, fire_echo 566) — caution (i) honored + commented. refresh writes runtime-BEFORE-manifest (a mid-write reader sees new-runtime/old-manifest at worst, never a runtime from an unseen manifest) — correct ordering. Poison-tolerant locks. reload-on-dirty seeds with the bringup hash so only a LATER swap fires; keep-last-good + don't-advance-last on unparseable; never kills the loop. from_toml_str = the parse front door (manifest.rs:5) and build_runtime is the same with_install_dir builder as bringup → reload reproduces bringup exactly (consistency holds). Brain-parity intact.
- clippy --workspace --all-targets -D warnings EXIT 0; 5 W3c units PASS (4 reload + cell-refresh); traceable EXIT 0 (+doc+impl+unit). Non-vacuity accepted (neuter refresh → red).

GREENLIGHT (a)+(c) batch:
- (a) stop-path scoped-reap: invoke psyche_orphan_should_reap (verbatim, fail-safe-decline) at reconcile stop-side (livehost.rs:280-301) + confirm_residency_or_unhost (477), after stop_host. MINT REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP (shape in W3-GATE-VERDICT.md §New REQ-HAZARD; stages [doc,impl,unit,int], doc = a KNOWN-HAZARDS.md row + the ADR-0025 amendment).
- (c) psyche own-copy: spawn detached psyche from <perch>/.live-bin/<prog>, copy-on-hash-differs (cheap re-spawn), torn down with perch; the REQ-INSTALL-11 unit (spawned path under .live-bin, bytes == install_dir binary).

Your W3D RIPPLE is confirmed coherent (none-live→CLI-direct CRC swap; some-live→KIND_ADAPTER_APPLY broker dispatch; stop translation+reap psyche→CRC swap→restart re-resolving from new manifest; brain self-heals via W3c reload; partial-failure rollback+restart-old; psyche own-copy means swap never hits a locked psyche). Build W3d AFTER (a)+(c). Ping me at the (a)+(c) gate. Strong work.
EOF
echo "---sent $?---"

Report W3c pass + greenlight (a)+(c) to todlando


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


W3c gated, (a)+(c) greenlit. Reframing task #8 (latent gap, not regression) + now resuming the grill the operator opened on this fix:


TaskUpdate

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.


Updated task #8 subject, description


Task #8 reframed. Now the grill. Let me read `establish_perch` to ground the home-resolution decision tree before the first question (does bind already read an existing endpoint's recorded home, or always demand it?):


183	fn establish_perch(
184	    id: &str,
185	    session_id: &str,
186	    parent_pid: u32,
187	    adapter: Option<&str>,
188	    subnet: Option<&str>,
189	    endpoint_type: &str,
190	    controllable: Option<bool>,
191	    cwd: Option<&str>,
192	) -> Result<String, BindError> {
193	    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
194
195	    let prior = info::read_info(&perch_path);
196	    if let Some(existing) = &prior {
197	        let owner_alive = info::read_pid(&perch_path)
198	            .map(proc::is_process_alive)
199	            .unwrap_or(false);
200	        if owner_alive && !existing.session_id.is_empty() && existing.session_id != session_id {
201	            return Err(BindError::Conflict {
202	                id: id.to_string(),
203	                held: existing.session_id.clone(),
204	            });
205	        }
206	    }
207
208	    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;
209	    // The endpoint type tag (info.json `state`). Defaults to the agent path
210	    // (`live_agent`); a non-agent endpoint (e.g. a `gateway` — the open type
211	    // system, REQ-EP-1/REQ-EP-6) binds with its own tag via `api bind --type`.
212	    // [impl->REQ-EP-6]
213	    // Open by construction: any tag round-trips through EndpointType::from_tag,
214	    // and downstream type-keyed logic (the user-msg identity gate, REQ-MSG-5)
215	    // reads it back verbatim. A revive keeps the prior type unless overridden.
216	    let endpoint_type = prior
217	        .as_ref()
218	        .map(|p| p.state.clone())
219	        .filter(|s| endpoint_type == "live_agent" && !s.is_empty())
220	        .unwrap_or_else(|| endpoint_type.to_string());
221	    let mut rec = InfoJson::new(
222	        id,
223	        &now_stamp(),
224	        std::process::id(),
225	        session_id,
226	        &endpoint_type,
227	    );
228	    rec.parent_pid = Some(parent_pid);
229	    let subnets = spt_store::subnet::SubnetStore::load();
230	    let mut vis = spt_store::visibility::VisibilityStore::load();
231	    let dirty = spt_store::home::stamp_creation_fields(
232	        &mut rec,
233	        prior.as_ref(),
234	        &subnets,
235	        &mut vis,
236	        subnet,
237	        adapter,
238	    )
239	    .map_err(|e| BindError::Home(e.to_string()))?;
240	    if dirty {
241	        let _ = vis.save();
242	    }
243	    // Broker-PTY-controllability stamp (REQ-PICKER-1): the caller knows the hosting
244	    // mode — `listen` is harness-hosted (no broker PTY → Some(false)); `bind` of a
245	    // live_agent is spt-hosted (broker PTY → Some(true)); a non-live bind is None.
246	    // Explicit wins; a revive that omits it carries the prior value forward (like
247	    // home/adapter — a re-bind must not wipe it).
248	    // [impl->REQ-PICKER-1]
249	    rec.controllable = controllable.or_else(|| prior.as_ref().and_then(|p| p.controllable));
250	    // W3 (REQ-HAZARD-BIND-CWD-UNSET): record the working directory so the picker
251	    // can group this endpoint under its PROJECT (data.rs → project_id_for_dir).
252	    // Previously NEVER set on bind — the refuted v0.12.1 "fresh agent shows under
253	    // its project" P1. spt-hosted: cmd_bind passes its own current_dir (the broker
254	    // spawned it in project_cwd); harness-hosted: bind_from_seed passes the seed's
255	    // captured cwd. A revive carries the prior cwd forward (a re-bind must not wipe
256	    // it), like home/adapter/controllable.
257	    // [impl->REQ-HAZARD-BIND-CWD-UNSET]
258	    rec.cwd = cwd
259	        .map(str::to_string)
260	        .or_else(|| prior.as_ref().and_then(|p| p.cwd.clone()));
261	    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;
262	    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
263	        .map_err(|e| BindError::Io(e.to_string()))?;
264
265	    // Session ledger (REQ-TERM-6): record this session as active so the digest
266	    // can span a later `/clear`|`/compact` boundary. The boot row dedups against
267	    // a same-session revive; a missing/unstamped session is not ledgered (the
268	    // digest spans only when there is a session to enumerate). Best-effort — a
269	    // ledger problem must never fail the bind.
270	    if !session_id.is_empty() {
271	        let _ = spt_store::sessions::append(
272	            &perch_path,

fn establish_perch(
    id: &str,
    session_id: &str,
    parent_pid: u32,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
    controllable: Option<bool>,
    cwd: Option<&str>,
) -> Result<String, BindError> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

    let prior = info::read_info(&perch_path);
    if let Some(existing) = &prior {
        let owner_alive = info::read_pid(&perch_path)
            .map(proc::is_process_alive)
            .unwrap_or(false);
        if owner_alive && !existing.session_id.is_empty() && existing.session_id != session_id {
            return Err(BindError::Conflict {
                id: id.to_string(),
                held: existing.session_id.clone(),
            });
        }
    }

    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;
    // The endpoint type tag (info.json `state`). Defaults to the agent path
    // (`live_agent`); a non-agent endpoint (e.g. a `gateway` — the open type
    // system, REQ-EP-1/REQ-EP-6) binds with its own tag via `api bind --type`.
    // [impl->REQ-EP-6]
    // Open by construction: any tag round-trips through EndpointType::from_tag,
    // and downstream type-keyed logic (the user-msg identity gate, REQ-MSG-5)
    // reads it back verbatim. A revive keeps the prior type unless overridden.
    let endpoint_type = prior
        .as_ref()
        .map(|p| p.state.clone())
        .filter(|s| endpoint_type == "live_agent" && !s.is_empty())
        .unwrap_or_else(|| endpoint_type.to_string());
    let mut rec = InfoJson::new(
        id,
        &now_stamp(),
        std::process::id(),
        session_id,
        &endpoint_type,
    );
    rec.parent_pid = Some(parent_pid);
    let subnets = spt_store::subnet::SubnetStore::load();
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let dirty = spt_store::home::stamp_creation_fields(
        &mut rec,
        prior.as_ref(),
        &subnets,
        &mut vis,
        subnet,
        adapter,
    )
    .map_err(|e| BindError::Home(e.to_string()))?;
    if dirty {
        let _ = vis.save();
    }
    // Broker-PTY-controllability stamp (REQ-PICKER-1): the caller knows the hosting
    // mode — `listen` is harness-hosted (no broker PTY → Some(false)); `bind` of a
    // live_agent is spt-hosted (broker PTY → Some(true)); a non-live bind is None.
    // Explicit wins; a revive that omits it carries the prior value forward (like
    // home/adapter — a re-bind must not wipe it).
    // [impl->REQ-PICKER-1]
    rec.controllable = controllable.or_else(|| prior.as_ref().and_then(|p| p.controllable));
    // W3 (REQ-HAZARD-BIND-CWD-UNSET): record the working directory so the picker
    // can group this endpoint under its PROJECT (data.rs → project_id_for_dir).
    // Previously NEVER set on bind — the refuted v0.12.1 "fresh agent shows under
    // its project" P1. spt-hosted: cmd_bind passes its own current_dir (the broker
    // spawned it in project_cwd); harness-hosted: bind_from_seed passes the seed's
    // captured cwd. A revive carries the prior cwd forward (a re-bind must not wipe
    // it), like home/adapter/controllable.
    // [impl->REQ-HAZARD-BIND-CWD-UNSET]
    rec.cwd = cwd
        .map(str::to_string)
        .or_else(|| prior.as_ref().and_then(|p| p.cwd.clone()));
    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;
    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
        .map_err(|e| BindError::Io(e.to_string()))?;

    // Session ledger (REQ-TERM-6): record this session as active so the digest
    // can span a later `/clear`|`/compact` boundary. The boot row dedups against
    // a same-session revive; a missing/unstamped session is not ledgered (the
    // digest spans only when there is a session to enumerate). Best-effort — a
    // ledger problem must never fail the bind.
    if !session_id.is_empty() {
        let _ = spt_store::sessions::append(
            &perch_path,


`establish_perch` calls `spt_store::home::stamp_creation_fields(rec, prior, subnets, vis, subnet, adapter)` — it receives `prior` (the existing perch). Whether HOME_REFUSED fires only on first-creation vs every bind depends on this. Read it:


Bash

sed -n '1,90p' crates/spt-store/src/home.rs

Read stamp_creation_fields home-resolution logic


//! Immutable home subnet (D9-5, REQ-INST-15 — ADR-0010, CONTEXT §home subnet).
//!
//! The home subnet anchors an endpoint's *defaults*: the default **sync**
//! scope (the creation flow seeds `sync_subnets = [home]` — the
//! [`crate::visibility`] documented seam, now real) and the subnet that
//! qualifies its bare name. **Assigned at creation, immutable for life**:
//!
//! - sole-subnet node → assigned automatically;
//! - multi-subnet node → must be specified (refuse-and-qualify, no silent
//!   guess — the resolution policy's stance);
//! - unpaired node → **local-only** (no home) until the node's *first join*
//!   sets it ([`adopt_for_unset`]).
//!
//! **No setter exists** — immutability by construction (ADR-0010: there is
//! no re-home; `spt fork` is the cross-subnet move). The only write paths
//! are the creation seams (stamping a *fresh* record) and
//! [`adopt_for_unset`], which by contract writes **only where home is
//! absent** — an existing assignment is never touched.
// [impl->REQ-INST-15]

use std::path::Path;

use crate::info;
use crate::perch::{self, ParentHint};
use crate::subnet::SubnetStore;
use crate::visibility::VisibilityStore;

/// Where a fresh endpoint's home lands.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HomeAssignment {
    /// The node resolved a home subnet (sole membership, or the explicit pick).
    Home(String),
    /// Unpaired node: the endpoint is local-only until the first join adopts it.
    LocalOnly,
}

/// Why home assignment refused (the creation must not proceed on a guess).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HomeError {
    /// An explicit `--subnet` named a subnet this node is not a member of.
    NotMember(String),
    /// Several subnets and no explicit pick — refuse-and-qualify. Lists them.
    Ambiguous(Vec<String>),
}

impl std::fmt::Display for HomeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            HomeError::NotMember(name) => {
                write!(
                    f,
                    "not a member of subnet '{name}' — cannot home an endpoint there"
                )
            }
            HomeError::Ambiguous(names) => write!(
                f,
                "this node holds {} subnets ({}) — pass --subnet <name> (home is assigned at \
                 creation, no guessing)",
                names.len(),
                names.join(", ")
            ),
        }
    }
}

impl std::error::Error for HomeError {}

/// Resolve a fresh endpoint's home against the node's subnet store
/// (CONTEXT §home subnet: auto-if-one / ask-if-many / local-only-if-none).
// [impl->REQ-INST-15]
pub fn assign_home(
    store: &SubnetStore,
    explicit: Option<&str>,
) -> Result<HomeAssignment, HomeError> {
    if let Some(name) = explicit {
        return if store.find(name).is_some() {
            Ok(HomeAssignment::Home(name.to_string()))
        } else {
            Err(HomeError::NotMember(name.to_string()))
        };
    }
    match store.subnets.len() {
        0 => Ok(HomeAssignment::LocalOnly),
        1 => Ok(HomeAssignment::Home(store.subnets[0].name.clone())),
        _ => Err(HomeError::Ambiguous(
            store.subnets.iter().map(|s| s.name.clone()).collect(),
        )),
    }
}


grep -n "fn stamp_creation_fields" crates/spt-store/src/home.rs; sed -n "$(grep -n 'fn stamp_creation_fields' crates/spt-store/src/home.rs | head -1 | cut -d: -f1),+45p" crates/spt-store/src/home.rs

Read stamp_creation_fields prior-handling


122:pub fn stamp_creation_fields(
268:    fn stamp_creation_fields_news_assign_and_revives_carry_forward() {
pub fn stamp_creation_fields(
    rec: &mut info::InfoJson,
    prior: Option<&info::InfoJson>,
    store: &SubnetStore,
    vis: &mut VisibilityStore,
    explicit_subnet: Option<&str>,
    adapter: Option<&str>,
) -> Result<bool, HomeError> {
    match prior {
        Some(p) => {
            rec.home_subnet = p.home_subnet.clone();
            rec.adapter = adapter.map(str::to_string).or_else(|| p.adapter.clone());
            rec.resources = p.resources.clone();
            rec.auto_suspend_after_ms = p.auto_suspend_after_ms;
            if rec.home_subnet.is_none() {
                // Legacy/unpaired-created record: adopt only the unambiguous
                // case — a revive must not start refusing.
                if let Ok(HomeAssignment::Home(h)) = assign_home(store, explicit_subnet) {
                    rec.home_subnet = Some(h.clone());
                    seed_endpoint_scopes(vis, &rec.id, &h);
                    return Ok(true);
                }
            }
            Ok(false)
        }
        None => {
            rec.adapter = adapter.map(str::to_string);
            match assign_home(store, explicit_subnet)? {
                HomeAssignment::Home(h) => {
                    rec.home_subnet = Some(h.clone());
                    seed_endpoint_scopes(vis, &rec.id, &h);
                    Ok(true)
                }
                HomeAssignment::LocalOnly => Ok(false),
            }
        }
    }
}

/// First-join adoption (CONTEXT: "on an unpaired node the endpoint is
/// local-only until first join, when home is set"): when the node holds
/// **exactly one** subnet, stamp it as home on every Self perch whose record
/// has none, seeding each one's sync scope alongside. Strictly
/// adopt-if-unset — a record with a home is untouched (immutability), and a
/// multi-subnet store adopts nothing (that would be a guess). Returns the
/// adopted endpoint ids; the caller persists `vis`.


Fully grounded now. The key finding sharpens the whole fix: `stamp_creation_fields` already **inherits the recorded home on re-bind** (prior-branch, immutable per ADR-0010) — so `HOME_REFUSED` fires **only on first creation of a fresh endpoint** on a multi-subnet node. Resume/re-bind is unaffected. The gap is narrow: first-bind home for the spt-hosted bringup.

Now the grill, question 1 (the foundational one — everything else is downstream mechanism):


AskUserQuestion

Question: On a multi-subnet node, how should `spt endpoint run` obtain the home subnet for a NEW endpoint? (Resume/re-bind already inherits the recorded home immutably — stamp_creation_fields prior-branch — so this is only the first-creation case, perri's wall-a.)

Choices:
endpoint run gains --subnet <name>; on a multi-subnet node with a fresh id and no flag, REFUSE before spawning the harness with a clear message (mirrors assign_home's existing Ambiguous policy). Minimal surface, operator states intent, consistent with the established 'no guessing' rule. Resume unaffected.
$ spt endpoint run --adapter claude-spt --id wall-a
MULTI_SUBNET_HOME: this node holds 2 subnets
(SPT_DEV, BIGNET) — pass --subnet <name>
(home is assigned at creation, no guessing)

$ spt endpoint run --adapter claude-spt --id wall-a --subnet SPT_DEV
ENDPOINT_RUN:wall-a ... ONLINE   # binds, no hook change
New `spt subnet use <name>` sets a node-level default home pointer; endpoint run falls back to it when --subnet is absent. Set-once UX, but introduces a NEW concept (node-default-home) on top of the per-endpoint home model — a CONTEXT.md glossary addition + more surface.
$ spt subnet use SPT_DEV      # set once
$ spt endpoint run --adapter claude-spt --id wall-a
ENDPOINT_RUN:wall-a ... ONLINE   # uses node default

# but: new persisted concept, new glossary term,
# interacts with the immutable per-endpoint home model
Reuse issuer_home_subnet / most-recently-active endpoint's home. But operator-invoked `endpoint run` has no clear issuer endpoint → edges toward the 'guessing' the policy explicitly refuses.
$ spt endpoint run --adapter claude-spt --id wall-a
# infers home from ... which endpoint?
# operator-invoked run has no issuer →
# ambiguous / guess-adjacent (policy says no)


Your questions have been answered: "On a multi-subnet node, how should `spt endpoint run` obtain the home subnet for a NEW endpoint? (Resume/re-bind already inherits the recorded home immutably — stamp_creation_fields prior-branch — so this is only the first-creation case, perri's wall-a.)"=(no option selected) notes: `spt endpoint run` creation flow should have a "pick home subnet" step on multi-subnet nodes. it should remember the most recently-chosen subnets, and list them most-to-least-recent, top-to-bottom. then, the chosen home subnet is assigned to the skeleton perch that harness ultimately binds to. -for the actual CLI usage of `spt endpoint run`, it should just default to the most recently-chosen subnet. this is OK because endpoints' home subnet can be changed in post. -a necessary change: currently, `spt endpoint run` doesn't rc into the fresh endpoint until after the harness has bound the perch. this needs changed: the PTY should be visible and controllable ASAP once the process has started. under the current architecture, certain things like a claude code user prompt could prevent the harness from binding the perch until the user clears the prompt.. You can now continue with these answers in mind.

Question: On a multi-subnet node, how should `spt endpoint run` obtain the home subnet for a NEW endpoint? (Resume/re-bind already inherits the recorded home immutably — stamp_creation_fields prior-branch — so this is only the first-creation case, perri's wall-a.)

Choices:
endpoint run gains --subnet <name>; on a multi-subnet node with a fresh id and no flag, REFUSE before spawning the harness with a clear message (mirrors assign_home's existing Ambiguous policy). Minimal surface, operator states intent, consistent with the established 'no guessing' rule. Resume unaffected.
$ spt endpoint run --adapter claude-spt --id wall-a
MULTI_SUBNET_HOME: this node holds 2 subnets
(SPT_DEV, BIGNET) — pass --subnet <name>
(home is assigned at creation, no guessing)

$ spt endpoint run --adapter claude-spt --id wall-a --subnet SPT_DEV
ENDPOINT_RUN:wall-a ... ONLINE   # binds, no hook change
New `spt subnet use <name>` sets a node-level default home pointer; endpoint run falls back to it when --subnet is absent. Set-once UX, but introduces a NEW concept (node-default-home) on top of the per-endpoint home model — a CONTEXT.md glossary addition + more surface.
$ spt subnet use SPT_DEV      # set once
$ spt endpoint run --adapter claude-spt --id wall-a
ENDPOINT_RUN:wall-a ... ONLINE   # uses node default

# but: new persisted concept, new glossary term,
# interacts with the immutable per-endpoint home model
Reuse issuer_home_subnet / most-recently-active endpoint's home. But operator-invoked `endpoint run` has no clear issuer endpoint → edges toward the 'guessing' the policy explicitly refuses.
$ spt endpoint run --adapter claude-spt --id wall-a
# infers home from ... which endpoint?
# operator-invoked run has no issuer →
# ambiguous / guess-adjacent (policy says no)


<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="872f0f34" type="msg" from="todlando">W3 (a)+(c) batch DONE @d6d7816 (pushed). The two-part psyche-lock fix for perri F-010xF-015. Ready for your gate.

(a) STOP-PATH SCOPED-REAP — minted REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP (your shape, registered in traceable-reqs.toml + KNOWN-HAZARDS.md 7.22; stages [doc,impl,unit], int=W3e step 3). Factore</EVENT-PART>
<EVENT-PART seq="2/8" id="872f0f34">d the per-id reap body reap_orphan_psyche_for (SHARED with the brain-start sweep — DRY, the guard psyche_orphan_should_reap is reused VERBATIM, fail-safe-decline). reap_stopped_endpoint_orphan_psyche (self-sourcing wrapper) called after set.stop_host at the reconcile stop-side (livehost.rs ~291) AND confirm_residency_or_unhost (~482). Belt-a</EVENT-PART>
<EVENT-PART seq="3/8" id="872f0f34">nd-suspenders: handle-reap frees this brain's owned psyche; the scoped reap catches a brain-less orphan the handle can't.

(c) PSYCHE OWN-COPY (primary) — psyche_spawn_runtime resolves the shipped psyche binary, copies it to &lt;perch&gt;/.live-bin/&lt;prog&gt; (copy_if_changed: copy-only-when-hash-differs via sha256_hex — cheap re-s</EVENT-PART>
<EVENT-PART seq="4/8" id="872f0f34">pawn), pins the spawn runtime at .live-bin. REQ-INSTALL-11 held (same bytes, relocated; args/strings/{key} still resolve against the real install_dir). Degrades to install-dir spawn on any gap (no dir / unresolvable / copy fail) — psyche must always spawn. .live-bin under the perch -&gt; torn down with it. Both spawn sites (spawn_psyche + sp</EVENT-PART>
<EVENT-PART seq="5/8" id="872f0f34">awn_psyche_owned) use it. Translation stays in-install-dir (justified — supervised, W3d stop-before-swap frees its lock).

EVIDENCE: 5 units. (a): reaps-a-brain-less-orphan + spares-an-unrelated-pid (fail-safe-decline). (c): copy-then-skip + spawn-runtime-lands-.live-bin-copy-with-identical-bytes (REQ-INSTALL-11 proof) + falls-back-wit</EVENT-PART>
<EVENT-PART seq="6/8" id="872f0f34">hout-install_dir. Non-vacuity: neuter copy_if_changed / force the reap-guard to decline -&gt; RED.

SCOPE AUDIT (the lesson from W3c): I git diff --stat'd the test subagent's output — this time it stayed clean (impl byte-identical; non-test additions are ONLY reap_orphan_psyche_for/reap_stopped_endpoint_orphan_psyche/2 call sites + cop</EVENT-PART>
<EVENT-PART seq="7/8" id="872f0f34">y_if_changed/psyche_spawn_runtime; everything else in the test mod). The explicit &quot;I'll git diff and revert&quot; warning worked.

GATES (ran myself): clippy --workspace --all-targets -D warnings clean; traceable exit 0 (289/289, REQ-HAZARD-STOP-PATH +doc+impl+unit, REQ-INSTALL-11 +unit added); xtask drift OK; 5 units pass (filtered</EVENT-PART>
<EVENT-PART seq="8/8" id="872f0f34">).

NEXT after your gate: W3d — the apply spanning both processes (KIND_ADAPTER_APPLY on the broker stops translation + CRC swaps; brain self-heals manifest via W3c reload-on-dirty; psyche own-copy means the
...(truncated)</event>
</task-notification>


Your answer reshapes the fix into a creation-flow redesign — and it surfaces a **direct glossary conflict** I have to challenge before going further (per the grill contract):

You said the chosen home "can be changed in post." But `CONTEXT.md` §home subnet + ADR-0010 + `home.rs` define home as **immutable**: *"Home is immutable — there is no re-home (ADR-0010). To place an agent's mind in a different subnet, fork it."* The immutability exists to avoid sync-scope / stale-mind ambiguity. This is the linchpin: if home is immutable, a **silent MRU default is risky** (wrong = stuck → must fork); if we relax it, silent-MRU-default is fine but ADR-0010's guarantees reopen.

I'm parking your **attach-ASAP** point (decouple rc/attach from bind so the PTY is visible before the harness binds — e.g. a CC prompt blocking bind) as a *distinct* item to grill after the home flow — it's real but orthogonal to home resolution. Flag if you want it folded in.

Question 2 (the linchpin):


Question: Your design says an endpoint's home subnet 'can be changed in post,' but ADR-0010 + CONTEXT define home as IMMUTABLE (no re-home; `spt fork` is the only cross-subnet move, to avoid sync-scope/stale-mind ambiguity). This determines whether a silent MRU default is safe. Which model do you want?

Choices:
Home stays immutable (ADR-0010 unchanged). CLI endpoint run defaults to the MRU subnet but PRINTS the chosen home (so a wrong default is caught immediately, corrected by re-running with --subnet or the picker). Interactive creation shows the MRU-ordered picker. No glossary/ADR change; wrong-pick is caught at creation, not silently locked.
$ spt endpoint run --adapter claude-spt --id wall-a
HOME: SPT_DEV (most-recent; --subnet to override)
ENDPOINT_RUN:wall-a ... ONLINE

# immutable after creation; fork to move.
# wrong default is VISIBLE at creation, not silent.
Supersede ADR-0010: home becomes changeable post-creation (a new `spt endpoint set-home`). Enables a fully silent MRU default (wrong = fixable later). But reopens the sync-scope re-binding + stale-mind ambiguity ADR-0010 closed, and `spt fork` loses its reason-to-exist. A big, hard-to-reverse decision needing its own ADR.
$ spt endpoint run --id wall-a   # silent MRU home
$ spt endpoint set-home wall-a BIGNET  # change later

# but: re-homing must migrate sync scope + bare-name
# qualification; stale-mind window; fork redundant.
# new ADR supersedes 0010.


Your questions have been answered: "Your design says an endpoint's home subnet 'can be changed in post,' but ADR-0010 + CONTEXT define home as IMMUTABLE (no re-home; `spt fork` is the only cross-subnet move, to avoid sync-scope/stale-mind ambiguity). This determines whether a silent MRU default is safe. Which model do you want?"=(no option selected) notes: keep immutable. deny missing `--subnet` if terminal environment is non-interactive. if it IS interactive, first present the proposed endpoint config (name, project, adapter[:profile], home subnet) and prompt for confirmation: "Ok to proceed? Y/n"-->if 'n', the resulting output informs of the `--subnet` option. You can now continue with these answers in mind.

Question: Your design says an endpoint's home subnet 'can be changed in post,' but ADR-0010 + CONTEXT define home as IMMUTABLE (no re-home; `spt fork` is the only cross-subnet move, to avoid sync-scope/stale-mind ambiguity). This determines whether a silent MRU default is safe. Which model do you want?

Choices:
Home stays immutable (ADR-0010 unchanged). CLI endpoint run defaults to the MRU subnet but PRINTS the chosen home (so a wrong default is caught immediately, corrected by re-running with --subnet or the picker). Interactive creation shows the MRU-ordered picker. No glossary/ADR change; wrong-pick is caught at creation, not silently locked.
$ spt endpoint run --adapter claude-spt --id wall-a
HOME: SPT_DEV (most-recent; --subnet to override)
ENDPOINT_RUN:wall-a ... ONLINE

# immutable after creation; fork to move.
# wrong default is VISIBLE at creation, not silent.
Supersede ADR-0010: home becomes changeable post-creation (a new `spt endpoint set-home`). Enables a fully silent MRU default (wrong = fixable later). But reopens the sync-scope re-binding + stale-mind ambiguity ADR-0010 closed, and `spt fork` loses its reason-to-exist. A big, hard-to-reverse decision needing its own ADR.
$ spt endpoint run --id wall-a   # silent MRU home
$ spt endpoint set-home wall-a BIGNET  # change later

# but: re-homing must migrate sync scope + bare-name
# qualification; stale-mind window; fork redundant.
# new ADR supersedes 0010.


Prompt:
<task-notification>
<task-id>b3ok5tzh4</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="872f0f34" type="msg" from="todlando">W3 (a)+(c) batch DONE @d6d7816 (pushed). The two-part psyche-lock fix for perri F-010xF-015. Ready for your gate.

(a) STOP-PATH SCOPED-REAP — minted REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP (your shape, registered in traceable-reqs.toml + KNOWN-HAZARDS.md 7.22; stages [doc,impl,unit], int=W3e step 3). Factore</EVENT-PART>
<EVENT-PART seq="2/8" id="872f0f34">d the per-id reap body reap_orphan_psyche_for (SHARED with the brain-start sweep — DRY, the guard psyche_orphan_should_reap is reused VERBATIM, fail-safe-decline). reap_stopped_endpoint_orphan_psyche (self-sourcing wrapper) called after set.stop_host at the reconcile stop-side (livehost.rs ~291) AND confirm_residency_or_unhost (~482). Belt-a</EVENT-PART>
<EVENT-PART seq="3/8" id="872f0f34">nd-suspenders: handle-reap frees this brain's owned psyche; the scoped reap catches a brain-less orphan the handle can't.

(c) PSYCHE OWN-COPY (primary) — psyche_spawn_runtime resolves the shipped psyche binary, copies it to &lt;perch&gt;/.live-bin/&lt;prog&gt; (copy_if_changed: copy-only-when-hash-differs via sha256_hex — cheap re-s</EVENT-PART>
<EVENT-PART seq="4/8" id="872f0f34">pawn), pins the spawn runtime at .live-bin. REQ-INSTALL-11 held (same bytes, relocated; args/strings/{key} still resolve against the real install_dir). Degrades to install-dir spawn on any gap (no dir / unresolvable / copy fail) — psyche must always spawn. .live-bin under the perch -&gt; torn down with it. Both spawn sites (spawn_psyche + sp</EVENT-PART>
<EVENT-PART seq="5/8" id="872f0f34">awn_psyche_owned) use it. Translation stays in-install-dir (justified — supervised, W3d stop-before-swap frees its lock).

EVIDENCE: 5 units. (a): reaps-a-brain-less-orphan + spares-an-unrelated-pid (fail-safe-decline). (c): copy-then-skip + spawn-runtime-lands-.live-bin-copy-with-identical-bytes (REQ-INSTALL-11 proof) + falls-back-wit</EVENT-PART>
<EVENT-PART seq="6/8" id="872f0f34">hout-install_dir. Non-vacuity: neuter copy_if_changed / force the reap-guard to decline -&gt; RED.

SCOPE AUDIT (the lesson from W3c): I git diff --stat'd the test subagent's output — this time it stayed clean (impl byte-identical; non-test additions are ONLY reap_orphan_psyche_for/reap_stopped_endpoint_orphan_psyche/2 call sites + cop</EVENT-PART>
<EVENT-PART seq="7/8" id="872f0f34">y_if_changed/psyche_spawn_runtime; everything else in the test mod). The explicit &quot;I'll git diff and revert&quot; warning worked.

GATES (ran myself): clippy --workspace --all-targets -D warnings clean; traceable exit 0 (289/289, REQ-HAZARD-STOP-PATH +doc+impl+unit, REQ-INSTALL-11 +unit added); xtask drift OK; 5 units pass (filtered</EVENT-PART>
<EVENT-PART seq="8/8" id="872f0f34">).

NEXT after your gate: W3d — the apply spanning both processes (KIND_ADAPTER_APPLY on the broker stops translation + CRC swaps; brain self-heals manifest via W3c reload-on-dirty; psyche own-copy means the
...(truncated)</event>
</task-notification>


Q2 captured — the home-resolution flow is now well-defined:
- **Home stays immutable** (ADR-0010 unchanged).
- Multi-subnet, no `--subnet`: **non-interactive → refuse** (clear `--subnet` guidance); **interactive → show proposed config** (name, project, adapter[:profile], home=MRU default) + `"Ok to proceed? Y/n"` → `n` → print `--subnet` guidance.
- Chosen home → the **skeleton perch** the harness binds to.

todlando's **W3 (a)+(c) landed** (@d6d7816) — fast-gating it now to keep v0.13.2 moving (todlando idles on W3d until I gate), then I'll pose grill Q3. Fetch + read:


Bash

git pull --ff-only --quiet 2>&1 | tail -1; echo "===HEAD==="; git log --oneline -1 | cat; echo "===diff stat 97ff869..d6d7816==="; git diff --stat 97ff869..d6d7816 | cat; echo "=== KNOWN-HAZARDS 7.22 + REQ mint ==="; grep -n "STOP-PATH-PSYCHE-ORPHAN-REAP" traceable-reqs.toml docs/KNOWN-HAZARDS.md | head

Fetch (a)+(c) + diff stat + REQ/hazard mint


===HEAD===
d6d7816 feat(daemon): W3 (a) stop-path psyche reap + (c) psyche own-copy (ADR-0025)
===diff stat 97ff869..d6d7816===
 crates/spt-daemon/src/lifecycle.rs | 223 +++++++++++++++++++++++++++-
 crates/spt-daemon/src/livehost.rs  | 296 ++++++++++++++++++++++++++++++++-----
 docs/KNOWN-HAZARDS.md              |   2 +
 traceable-reqs.toml                |   4 +
 4 files changed, 486 insertions(+), 39 deletions(-)
=== KNOWN-HAZARDS 7.22 + REQ mint ===
traceable-reqs.toml:1346:id = "REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP"
docs/KNOWN-HAZARDS.md:531:<!-- [doc->REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP] -->