$OWL send doyle <<'EOF'
L0 SEAM-GREEN @cf5eab4 (branch v0.12.1-lifecycle). Option (a) implemented exactly as ruled.

FIX: serve_attach forwards on a SEPARATE broker conn (cold_start) from the one it subscribes/receives on. All 7 wire sends (Output/Size/Exit/Displaced/refuse-finish/eof-finish) -> the wire conn; receives (subscribe/attach_as/send_effect/resize/detach_session) stay on brain. Order preserved (one serve thread, seq order in, same order out). delivered_through cursor untouched. broker_name threaded to serve_attach + dispatch + 4 test call sites.

EVIDENCE (instrumented confirmation): pre-fix the broker NEVER ran send_stream(stream=1) for serve's forwards (op_r read pump 0 reads, rc 0 bytes). Post-fix: rc_saw_tick=true, 230 bytes delivered, run 13s->2.8s.

REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT activated [impl,unit,int], tagged in-commit:
- impl: serve_attach two-conn split.
- unit: attach.rs loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock (non-empty ring at attach doesn't wedge the forward path — the missing regression).
- int: dummy_harness_e2e rc_saw_tick HARD ASSERT (live already-producing harness delivers tick). Old rc_connected proxy subsumed -> informational (it raced the now-fast teardown).

RECORDED in commit + REQ: controller does SYNC inline replay vs viewers' async writer-thread+channel = the deeper root; Option (b) = unify controller onto async pattern, flagged POST-v0.12.1 (touches cursor path). Cross-node = covered-by-construction (same single-conn multiplex); LOCAL gated now, no twohost run blocked on it.

GATE-GREEN LOCAL: attach 13/13, dummy fixture green, clippy --workspace -D warnings clean, traceable EXIT=0.

Proceeding to L1 (VIEWER-CLOSE-DETACH, CREATE_BREAKAWAY_FROM_JOB + daemon-owned Job backstop) then L2 (ATTACH-WEDGE) per the JIT. Will ping SEAM-GREEN per wave for your independent re-run.
EOF

Ping doyle L0 SEAM-GREEN


➜ SENT:doyle


Update memory: L0 FIXED + committed.


Edit

**Fix options sent to doyle (awaiting design ruling):**

**SHIPPED (FIXED + committed @cf5eab4, branch v0.12.1-lifecycle):** doyle ruled Option (a). `serve_attach` now forwards all 7 wire sends on a SEPARATE `Brain::cold_start` conn from the one it subscribes/receives on (`broker_name` threaded to serve_attach + dispatch + 4 test sites). Order preserved (one serve thread), `delivered_through` cursor untouched. Result: rc_saw_tick=true, 230 bytes delivered, run 13s→2.8s. REQ activated [impl,unit,int]; unit = `loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock`; int = dummy fixture `rc_saw_tick` hard assert (old `rc_connected` proxy now informational — raced the fast teardown). Gate-green local: attach 13/13, dummy green, clippy --workspace clean, traceable EXIT=0. SEAM-GREEN pinged to doyle for independent re-run. Option (b) (unify controller replay onto viewer async writer-thread pattern) flagged POST-v0.12.1 in commit+REQ. NEXT: L1 VIEWER-CLOSE-DETACH + L2 ATTACH-WEDGE.

**Original fix options (doyle picked a):**


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\v0121-l0-attach-deadlock-root.md has been updated successfully. (file state is current in your context — no need to Read it back)

**SHIPPED (FIXED + committed @cf5eab4, branch v0.12.1-lifecycle):** doyle ruled Option (a). `serve_attach` now forwards all 7 wire sends on a SEPARATE `Brain::cold_start` conn from the one it subscribes/receives on (`broker_name` threaded to serve_attach + dispatch + 4 test sites). Order preserved (one serve thread), `delivered_through` cursor untouched. Result: rc_saw_tick=true, 230 bytes delivered, run 13s→2.8s. REQ activated [impl,unit,int]; unit = `loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock`; int = dummy fixture `rc_saw_tick` hard assert (old `rc_connected` proxy now informational — raced the fast teardown). Gate-green local: attach 13/13, dummy green, clippy --workspace clean, traceable EXIT=0. SEAM-GREEN pinged to doyle for independent re-run. Option (b) (unify controller replay onto viewer async writer-thread pattern) flagged POST-v0.12.1 in commit+REQ. NEXT: L1 VIEWER-CLOSE-DETACH + L2 ATTACH-WEDGE.

**Original fix options (doyle picked a):**

---
name: v0121-l0-attach-deadlock-root
description: v0.12.1 L0 keystone REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT root cause — serve_attach replay-vs-forward IPC deadlock
metadata:
  node_type: memory
  type: project
  originSessionId: 3acfe970-b087-4764-8a3e-c2253b8bd0ae
---

v0.12.1 Wave 2 **L0 KEYSTONE root-caused** (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT, the operator's "attach shows no output"). Branch v0.12.1-lifecycle @0d8ed2e (doyle's minted REQ). Found via instrumenting the real dummy fixture (`dummy_harness_e2e.rs`), NOT static reading.

**ROOT (definitive):** a broker REPLAY-vs-FORWARD IPC deadlock in `serve_attach`, triggered ONLY by a non-empty `OutputLog` ring at attach = an already-producing harness = exactly endpoint-run. `serve_attach` receives session Output (broker→serve) AND sends its forwards (serve→broker) on the **same** broker IPC connection. `become_controller` (broker.rs ~249) does a synchronous **multi-frame** ring replay inline inside `dispatch_subscribe`, holding the broker's single-threaded per-conn handler in the broker→serve write loop → it can't read serve's serve→broker `net_stream_send` forwards → both directions back up → mutual backpressure deadlock → operator (rc) gets 0 bytes.

**Ruled OUT:** drain works (ring fills to 9), not the dummy program, not worker starvation (tried net runtime 2→8 workers, nethost.rs:653, no change). Smoking gun: serve prints SERVE_FWD seq0/seq1 but broker-side `send_stream(stream=1)` NEVER runs → loopback op_r read pump never reads.

**Why green-but-broken:** known-good `local_attach_via_loopback_conn_rides_the_same_pump` attaches to an IDLE echo child (empty ring → 0-frame replay) and forwards ONE chunk; cwd E2E reads on the spawning conn (no serve-over-IPC). Neither exercises full-ring replay + sustained forward on one conn. **Likely cross-node too** (same serve_attach shape over QUIC).

**Fix options sent to doyle (awaiting design ruling):** (a) serve_attach forwards on a SEPARATE broker conn from the one it subscribes on — surgical, `send_stream` is stream_id-keyed/conn-agnostic; my lean for v0.12.1. (b) make `become_controller` replay non-blocking via the dedicated-writer-thread+bounded-channel pattern `add_viewer`/`viewer_writer` already use — more complete (fixes cross-node) but touches the authoritative cursor-advancing controller path. Then activate REQ stages [impl,unit,int] + flip fixture `rc_saw_tick` diagnostic → hard assert. See [[v0121-lifecycle-realharness]] [[v0121-realharness-reopen]].

-**Fix options sent to doyle (awaiting design ruling):** (a) serve_attach forwards on a SEPARATE broker conn from the one it subscribes on — surgical, `send_stream` is stream_id-keyed/conn-agnostic; my lean for v0.12.1. (b) make `become_controller` replay non-blocking via the dedicated-writer-thread+bounded-channel pattern `add_viewer`/`viewer_writer` already use — more complete (fixes cross-node) but touches the authoritative cursor-advancing controller path. Then activate REQ stages [impl,unit,int] + flip fixture `rc_saw_tick` diagnostic → hard assert. See [[v0121-lifecycle-realharness]] [[v0121-realharness-reopen]].

+**SHIPPED (FIXED + committed @cf5eab4, branch v0.12.1-lifecycle):** doyle ruled Option (a). `serve_attach` now forwards all 7 wire sends on a SEPARATE `Brain::cold_start` conn from the one it subscribes/receives on (`broker_name` threaded to serve_attach + dispatch + 4 test sites). Order preserved (one serve thread), `delivered_through` cursor untouched. Result: rc_saw_tick=true, 230 bytes delivered, run 13s→2.8s. REQ activated [impl,unit,int]; unit = `loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock`; int = dummy fixture `rc_saw_tick` hard assert (old `rc_connected` proxy now informational — raced the fast teardown). Gate-green local: attach 13/13, dummy green, clippy --workspace clean, traceable EXIT=0. SEAM-GREEN pinged to doyle for independent re-run. Option (b) (unify controller replay onto viewer async writer-thread pattern) flagged POST-v0.12.1 in commit+REQ. NEXT: L1 VIEWER-CLOSE-DETACH + L2 ATTACH-WEDGE.

+**Original fix options (doyle picked a):** (a) serve_attach forwards on a SEPARATE broker conn from the one it subscribes on — surgical, `send_stream` is stream_id-keyed/conn-agnostic; my lean for v0.12.1. (b) make `become_controller` replay non-blocking via the dedicated-writer-thread+bounded-channel pattern `add_viewer`/`viewer_writer` already use — more complete (fixes cross-node) but touches the authoritative cursor-advancing controller path. Then activate REQ stages [impl,unit,int] + flip fixture `rc_saw_tick` diagnostic → hard assert. See [[v0121-lifecycle-realharness]] [[v0121-realharness-reopen]].


L0 keystone **shipped** (committed @cf5eab4, gate-green, doyle pinged, memory saved). Moving to **L1 (VIEWER-CLOSE-DETACH)**. Begin by reading the daemon spawn paths + existing Job Object/reap code.


680	        std_output: isize,
681	        std_error: isize,
682	    }
683	    #[repr(C)]
684	    struct ProcessInformation {
685	        process: isize,
686	        thread: isize,
687	        pid: u32,
688	        tid: u32,
689	    }
690	    extern "system" {
691	        fn CreateProcessW(
692	            app: *const u16,
693	            cmdline: *mut u16,
694	            process_attrs: *mut core::ffi::c_void,
695	            thread_attrs: *mut core::ffi::c_void,
696	            inherit_handles: i32,
697	            flags: u32,
698	            env: *mut core::ffi::c_void,
699	            cwd: *const u16,
700	            startup: *mut StartupInfoW,
701	            out: *mut ProcessInformation,
702	        ) -> i32;
703	        fn CloseHandle(h: isize) -> i32;
704	    }
705	    // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW: no
706	    // inherited console, no popup window, its own signal group.
707	    const FLAGS: u32 = 0x0000_0008 | 0x0000_0200 | 0x0800_0000;
708
709	    let mut cmdline = String::new();
710	    quote_windows_arg(&mut cmdline, program);
711	    for a in args {
712	        cmdline.push(' ');
713	        quote_windows_arg(&mut cmdline, a);
714	    }
715	    let mut cmdline_w: Vec<u16> = std::ffi::OsStr::new(&cmdline)
716	        .encode_wide()
717	        .chain(std::iter::once(0))
718	        .collect();
719	    let mut si: StartupInfoW = unsafe { std::mem::zeroed() };
720	    si.cb = std::mem::size_of::<StartupInfoW>() as u32;
721	    let mut pi: ProcessInformation = unsafe { std::mem::zeroed() };
722	    let ok = unsafe {
723	        CreateProcessW(
724	            std::ptr::null(),
725	            cmdline_w.as_mut_ptr(),
726	            std::ptr::null_mut(),
727	            std::ptr::null_mut(),
728	            0, // bInheritHandles = FALSE — the whole point.
729	            FLAGS,
730	            std::ptr::null_mut(), // inherit this process's environment
731	            std::ptr::null(),
732	            &mut si,
733	            &mut pi,
734	        )
735	    };
736	    if ok == 0 {
737	        return Err(io::Error::last_os_error());
738	    }
739	    unsafe {
740	        CloseHandle(pi.process);
741	        CloseHandle(pi.thread);
742	    }
743	    Ok(pi.pid)
744	}
745
746	/// Append `arg` to a Windows command line under `CommandLineToArgvW` quoting
747	/// rules (quote when needed; double preceding backslashes before an embedded
748	/// quote; double trailing backslashes before the closing quote).
749	// [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]

        std_output: isize,
        std_error: isize,
    }
    #[repr(C)]
    struct ProcessInformation {
        process: isize,
        thread: isize,
        pid: u32,
        tid: u32,
    }
    extern "system" {
        fn CreateProcessW(
            app: *const u16,
            cmdline: *mut u16,
            process_attrs: *mut core::ffi::c_void,
            thread_attrs: *mut core::ffi::c_void,
            inherit_handles: i32,
            flags: u32,
            env: *mut core::ffi::c_void,
            cwd: *const u16,
            startup: *mut StartupInfoW,
            out: *mut ProcessInformation,
        ) -> i32;
        fn CloseHandle(h: isize) -> i32;
    }
    // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW: no
    // inherited console, no popup window, its own signal group.
    const FLAGS: u32 = 0x0000_0008 | 0x0000_0200 | 0x0800_0000;

    let mut cmdline = String::new();
    quote_windows_arg(&mut cmdline, program);
    for a in args {
        cmdline.push(' ');
        quote_windows_arg(&mut cmdline, a);
    }
    let mut cmdline_w: Vec<u16> = std::ffi::OsStr::new(&cmdline)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let mut si: StartupInfoW = unsafe { std::mem::zeroed() };
    si.cb = std::mem::size_of::<StartupInfoW>() as u32;
    let mut pi: ProcessInformation = unsafe { std::mem::zeroed() };
    let ok = unsafe {
        CreateProcessW(
            std::ptr::null(),
            cmdline_w.as_mut_ptr(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
            0, // bInheritHandles = FALSE — the whole point.
            FLAGS,
            std::ptr::null_mut(), // inherit this process's environment
            std::ptr::null(),
            &mut si,
            &mut pi,
        )
    };
    if ok == 0 {
        return Err(io::Error::last_os_error());
    }
    unsafe {
        CloseHandle(pi.process);
        CloseHandle(pi.thread);
    }
    Ok(pi.pid)
}

/// Append `arg` to a Windows command line under `CommandLineToArgvW` quoting
/// rules (quote when needed; double preceding backslashes before an embedded
/// quote; double trailing backslashes before the closing quote).
// [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]


L1 is delicate `unsafe` Windows Job Object work (CREATE_BREAKAWAY_FROM_JOB in `daemon.rs:707` `detached_no_inherit` + `deelevate.rs` elevated path + a daemon-owned Job backstop mirroring `reap.rs`). L0 is a complete, committed, gate-green checkpoint and my context is high from the deep root-cause work — this is the CLAUDE.md self-clear boundary. Mark L0 done in the JIT, commune, then self-clear to tackle L1 fresh.


- **L0 — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real

- **L0 — DONE @cf5eab4 (gate-green local, doyle SEAM-GREEN pinged for independent re-run).** ROOT was NOT
  candidates a/b/c/d: a REPLAY-vs-FORWARD IPC deadlock in `serve_attach` (received session output AND sent wire
  forwards on the SAME broker conn; `become_controller`'s sync inline ring replay blocked the single-threaded
  conn handler from reading the forwards). FIX (Option a, doyle ruling): serve_attach forwards on a SEPARATE
  broker conn. REQ activated [impl,unit,int]. Deeper root (controller sync-replay vs viewer async pattern) =
  Option (b), flagged POST-v0.12.1. See memory [[v0121-l0-attach-deadlock-root]]. **NEXT = L1.**

- **L0 (original spec) — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real


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

- **L0 — DONE @cf5eab4 (gate-green local, doyle SEAM-GREEN pinged for independent re-run).** ROOT was NOT
  candidates a/b/c/d: a REPLAY-vs-FORWARD IPC deadlock in `serve_attach` (received session output AND sent wire
  forwards on the SAME broker conn; `become_controller`'s sync inline ring replay blocked the single-threaded
  conn handler from reading the forwards). FIX (Option a, doyle ruling): serve_attach forwards on a SEPARATE
  broker conn. REQ activated [impl,unit,int]. Deeper root (controller sync-replay vs viewer async pattern) =
  Option (b), flagged POST-v0.12.1. See memory [[v0121-l0-attach-deadlock-root]]. **NEXT = L1.**

- **L0 (original spec) — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real

# v0.12.1 — Lifecycle + Picker JIT Plan

**Why:** v0.12.0 shipped (counter 25) but real-harness testing (operator, `claude-spt`, Windows) showed the
lifecycle fixes do **not** deliver in the real daemon + broker + PTY path. v0.12.0 was gated on **mock adapters +
in-process `reconcile_once`** — green tests, broken reality. v0.12.1 reopens with **real-harness gating** (no mocks).

**Branch:** `v0.12.1-lifecycle` off `main@5f9ea23`. **Release:** v0.12.1 PATCH (deployah, after doyle gate-pass all waves).
**Roles:** doyle designs/mints/gates · todlando executes · deployah releases.

**Meta-fix (binding for this sprint):** every lifecycle gate runs against the **real dummy-harness fixture + a real
detached daemon** — NOT `reconcile_once` in-proc, NOT a mock with a live-draining peer. doyle independently re-runs each.

---

## Wave 1 — Test infrastructure (unblocks all real-harness gates)

- **T1 — dummy-harness adapter fixture** (operator's idea). A `kind="harness"` adapter whose command is a trivial program
  that (a) binds its perch on startup (the harness contract), (b) prints a stdout line on an interval, (c) stays alive
  until killed. Drives the REAL `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc` attach path.
  Isolates "broker wedged / endpoint died" from "a real harness failed to launch." Becomes the permanent regression
  fixture the v0.12.0 mock tests never had. Lives in the test tree; runs against a scratch target dir (livehost-E2E pattern).

## Wave 2 — Lifecycle core

- **L0 — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real
  dummy-harness fixture: a clean `spt rc` attach to a LIVE, heartbeating, psyche-hosted `endpoint run` harness receives
  **0 bytes** over 10s of its flushed `[session.self]` stdout — no death, no wedge. This IS the operator's central
  "attach shows no output," and it blocks the whole "view is independent" goal (re-attach shows nothing). Known-good
  (attach.rs loopback-attach E2Es) proves the broker drains+fans a `spawn_session` PTY child over the same transport →
  the gap is endpoint-run-specific (both paths share `dispatch_spawn`, broker.rs:706/835). ISOLATE path-vs-program first
  (run the attach.rs known-delivering child as the endpoint-run `[session.self]`); then root the mechanism — candidates:
  (a) `spawn_session_pid` SpawnReq stdio/env/cwd diff; (b) harness stdout write-blocks on a full ConPTY buffer (drain not
  reading THIS pty) → alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6); (d) `rc` subscribe/`resolve_session` for an
  endpoint-run session reads the wrong/empty log. GATE (dummy harness): rc attach to a LIVE endpoint-run harness RECEIVES
  its DUMMY_HARNESS_TICK within a bounded window.

- **L1 — REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY).** Closing the tab/window where `spt endpoint run` was invoked must
  detach only the `spt rc` pump; the daemon-hosted harness keeps running and is re-attachable.
  ROOT: the daemon never breaks away from the launching terminal's Windows Job Object (`KILL_ON_JOB_CLOSE`); no
  `CREATE_BREAKAWAY_FROM_JOB` anywhere → tab close reaps the daemon's freshly-spawned ConPTY harness subtree. ConPTY
  isolation itself is already correct (portable-pty makes the pseudoconsole in the daemon; no console signal/handle leak).
  FIX: add `CREATE_BREAKAWAY_FROM_JOB` to both daemon spawn paths (`daemon.rs:707` `detached_no_inherit`,
  `deelevate.rs:519` elevated) **+** pin each broker-spawned harness into a **daemon-owned Job Object** (mirror Breap,
  `reap.rs`) as backstop (survives even if a terminal sets `SILENT_BREAKAWAY_OK=false`). `pty.rs` unchanged. Unix: verify
  the daemon's session-detach already covers terminal-close (SIGHUP scope) — likely no change, add a guard test.
  GATE: spawn daemon under a parent-held `KILL_ON_JOB_CLOSE` job → `endpoint run` a dummy harness → close the parent job →
  assert the harness pid **stays alive** AND `spt rc <id>` re-attaches AND a brand-new endpoint launches.

- **L2 — REQ-HAZARD-ATTACH-WEDGE (ROBUSTNESS).** Even a *legitimately* dead PTY child (real crash/kill) + an undrained
  operator pump must NOT wedge the broker.
  ROOT: loopback attach output is a blocking `write_all` into a bounded 64 KB tokio duplex (`nethost.rs:1040,1090`); a
  dead operator (closed tab) stops draining → `write_all` blocks forever (the "loopback never hangs" assumption at
  `nethost.rs:1103` is false) → parks workers in the **2-worker** net runtime (`nethost.rs:640`) → both saturate → every
  new attach/`endpoint run` stalls after `PUMP_IPC_READER: spawned` → 30 s `FIRST_EVENT_GRACE` → "dead or wedged";
  `daemon stop` can't join the stuck workers. Distinct from the removed B1 path-(c) mutex deadlock.
  FIX: make loopback sends fail-fast — a full-buffer / `BrokenPipe` loopback write is an ordinary per-stream error that
  ENDS `serve_attach`; one dead stream can never hold a runtime worker. (Defense-in-depth: raise worker count — but the
  real fix is non-blocking-on-dead-peer.)
  GATE (dummy harness): kill the child abruptly + drop the operator pump without a clean detach → assert a **new**
  endpoint is still served, `brain.sessions()` returns promptly, `daemon stop` completes bounded.

- **L3 — status=online persistence (sub-item, folded into L2's gate).** Three dead endpoints stayed `status=online`.
  B2 (`reconcile_hosted_liveness`) should offline a controllable spt-hosted perch whose broker session is gone — confirm
  whether abrupt Windows child death actually reaps the broker session (so B2 sees it absent) and whether the reconcile
  tick fires. Resolve as part of L2; GATE asserts the dead endpoint is marked **offline within one reconcile tick**.

- **L4 — `daemon stop` ends everything (operator item #1, folded).** `daemon stop` did not end all spt processes / take
  everything offline. Largely downstream of L1+L2 (un-wedged broker can stop cleanly; harness in a daemon-owned job is
  reaped by Breap). GATE: after L1+L2, `daemon stop` terminates the daemon + brain + all hosted harness/psyche processes,
  bounded, and the roster goes offline.

## Wave 3 — Picker (operator-raised; ROOTS TBD — investigate before fixing)

- **P1 — REQ-PICKER-HISTORY-FRESH.** `spt endpoint run` picker does **not** show project history for fresh endpoints.
  Investigate the project-history loader (v0.10.0 PICKER-2, `picker/data.rs`) — real bug vs "fresh = no history yet"
  semantics. Then fix.
- **P2 — REQ-PICKER-ONLINE-ACTION.** Picker shows **"Start now"** for endpoints that are already online. Investigate the
  4-state status mapping (v0.10.0 PICKER-1, `picker/model.rs`) — is it reading live/online state correctly, or rendering
  stale/wedged broker state? Then fix (online → "Attach", not "Start now").

## Wave 4 — endpoint list cleanup (operator decision 2026-06-17, dropped — now minted)

- **E1 — REQ-ENDPOINT-LIST-MERGE-LOCAL.** Remove the `--local` flag; the bare `spt endpoint list` ALWAYS merges this
  node's local (unadvertised) perches into the view. Rationale: `spt whoami` is a thin alias — a just-online agent
  running `whoami` must see its own perch. Drop the flag + its `--detail` conflict test + the v0.10.0 hint line +
  `cmd_list_local`; fix the `whoami` alias path. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target).

## Release

- **v0.12.1 PATCH.** deployah drives after doyle gate-pass on all waves. CHANGELOG: cross-check **each bullet's scope vs
  the commit range** (the v0.12.0 lesson — [[changelog-scope-vs-commit-range]]); name the specific delta. Post-publish:
  inform operator + perri (real-harness lifecycle now actually works).

## Open / carried

- Non-blocking test-hardening: drain OUTPUT after `KIND_EXIT` in `broker::spawn_env_reaches_child` (latent flake) — fold
  into Wave 1/2 test work if cheap, else carry.
- Machine cleanup: clear the operator's 3 phantom endpoints (wall-a/b/c) + orphan psyche — scoped, preserve daemon +
  the `doyle` live perch + CI.

-- **L0 — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real

+- **L0 — DONE @cf5eab4 (gate-green local, doyle SEAM-GREEN pinged for independent re-run).** ROOT was NOT

+  candidates a/b/c/d: a REPLAY-vs-FORWARD IPC deadlock in `serve_attach` (received session output AND sent wire

+  forwards on the SAME broker conn; `become_controller`'s sync inline ring replay blocked the single-threaded

+  conn handler from reading the forwards). FIX (Option a, doyle ruling): serve_attach forwards on a SEPARATE

+  broker conn. REQ activated [impl,unit,int]. Deeper root (controller sync-replay vs viewer async pattern) =

+  Option (b), flagged POST-v0.12.1. See memory [[v0121-l0-attach-deadlock-root]]. **NEXT = L1.**

+- **L0 (original spec) — REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (KEYSTONE — do FIRST).** Confirmed v0.12.1 Wave 1 via the real


Commune the state, then self-clear to resume L1 fresh.


Skill

v0.12.1 lifecycle (doyle sprint). L0 KEYSTONE DONE+COMMITTED @cf5eab4 on branch v0.12.1-lifecycle.

L0 = REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (operator's "attach shows no output"). ROOT (instrumented on real dummy fixture, NOT candidates a/b/c/d): replay-vs-forward IPC deadlock in serve_attach — it received session output AND sent wire forwards on the SAME broker conn; become_controller's sync inline ring replay blocked the single-threaded conn handler from reading serve's forwards -> mutual backpressure -> rc 0 bytes. Only fires with NON-EMPTY ring at attach = already-producing harness = endpoint-run. FIX (doyle Option a): serve_attach forwards on a SEPARATE Brain::cold_start conn (broker_name threaded to serve_attach + dispatch + 4 test sites); order preserved, delivered_through untouched. REQ activated [impl,unit,int]: impl=two-conn split; unit=attach.rs loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock; int=dummy_harness_e2e rc_saw_tick hard assert (230 bytes delivered, 13s->2.8s). GATE-GREEN LOCAL: attach 13/13, dummy green, clippy --workspace -D warnings clean, traceable EXIT=0. doyle pinged SEAM-GREEN (he re-runs independently). Deeper root recorded: controller sync-replay vs viewer async writer-thread = Option b, POST-v0.12.1.

NEXT = L1 (REQ-HAZARD-VIEWER-CLOSE-DETACH): closing the tab where `endpoint run` was invoked must detach only rc, daemon-hosted harness keeps running. ROOT: no CREATE_BREAKAWAY_FROM_JOB -> terminal Job Object (KILL_ON_JOB_CLOSE) reaps the daemon's harness subtree on tab-close. FIX: add CREATE_BREAKAWAY_FROM_JOB (0x0100_0000) to BOTH daemon spawn paths (daemon.rs:707 detached_no_inherit FLAGS, deelevate.rs:519 elevated) + pin each broker-spawned harness into a daemon-OWNED Job Object (mirror Breap, reap.rs) as backstop. Unix: verify session-detach already covers terminal-close (likely no change + guard test). GATE: spawn daemon under a parent-held KILL_ON_JOB_CLOSE job -> endpoint run dummy -> close parent job -> assert harness pid STAYS ALIVE + rc re-attaches + new endpoint launches. THEN L2 (ATTACH-WEDGE, loopback fail-fast nethost.rs:1040/1090/1103) + status-online-within-tick + daemon-stop-ends-all.

SEAM BAR every wave: pre-build mock-session (cargo build -p mock-adapter --bin mock-session into target-seam), clippy --workspace -D warnings + traceable EXIT=0 + dummy E2E (serial, NET-ENABLED, target-seam target dir). Full plan = V0.12.1-LIFECYCLE-JIT.md on branch (L0 marked DONE). Detail: memory [[v0121-l0-attach-deadlock-root]] [[v0121-lifecycle-realharness]] [[v0121-realharness-reopen]]. doyle gates per-wave; reply to his dispatches.


Launching skill: spt:commune


Base directory for this skill: C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\skills\commune

# /spt:commune

All commands use `$LIVE` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Flow

1. **Download Psyche's *current* context** — it shifts during the session as background echo communes fire. Re-download right before composing; the gen-N snapshot you read at session start is already stale:
   ```bash
   $LIVE psyche-download <your-id>
   ```

   > **Output truncation.** If the Bash tool reports `Output too large (N KB)` and saves the full result to a `tool-results/<id>.txt` file, use the `Read` tool on that saved path to access the full content. Do NOT re-run `psyche-download` to grep/awk out sections — repeated invocations re-download the same context, waste tokens, and may duplicate `## Pending Commune` sections in `live_context.md`.

2. **Diff your knowledge against this fresh download** — only what's missing from *it* is the real delta (new work, decisions, intentions). Anything already absorbed is not. If the download includes a `<memformat>` section, use it as a guide for what topics to cover. A `## Pending Commune (uncommitted)` section means a previous commune is still queued — do NOT re-send it.

3. **Commune only the delta** -- use the **Write tool** to create
   `.claude/{your-id}-commune.md` with the two-slice body as its contents
   (see `## Two-slice body shape (Phase 25 D-10/D-11)` below for the required
   envelope wrapping). Do not use Bash/heredoc -- Write is the canonical path:
   no shell escaping, no `EOF` collision risk, and Write auto-creates the
   `.claude/` parent directory.

   Your Self listener detects the file on its next poll iteration, notifies
   your Psyche wrapper, and the wrapper ingests the content via the existing
   Psyche session. On success (subprocess exit code 0) the wrapper deletes the
   file. On error (token rate limit, etc) the file is retained and retried on
   the next consume cycle. If you write again before the wrapper consumes, the
   latest write wins (last-write-wins overwrite semantics).

If Psyche's context is already current, no commune is needed. Say so and move on.

Read `commune.md` (in this skill directory) for the full commune protocol -- when to send, what to include, and diligence triggers.

**Agent-type guard:** Only works on live agents. If target is not live, refuses with: "Communes require a live agent with Psyche."

## Two-slice body shape (Phase 25 D-10/D-11)

The body you write in `.claude/{your-id}-commune.md` MUST itself be wrapped in two-slice envelopes per Phase 25 D-10/D-11. The taxonomy — what belongs in `<live-context>` vs `<project-context>` — is defined canonically in `psyche.md` §`<output_envelope>` (lines 282–323). Read that section as the source of truth; this skill doc teaches only when and how to apply it.

**In-project detection rule (D-25.1-04).** Before composing your commune body, run `$LIVE psyche-download <your-id>` (Step 1 of `## Flow` above) and inspect the `<current ... />` tag at the top of the output. The `<current/>` tag carries a `project="..."` attribute whenever any payload is produced — its presence means you are inside a tracked project (inside a git repo, `project` is the repo name; outside a git repo per D-11, `project` is the cwd-basename fallback, which still counts as a project context for routing purposes). A `psyche-download` invocation that returns no payload at all (NO-CONTEXT exit) means no project resolved. So: `project="..."` populated on `<current/>` → emit BOTH envelopes (apply rule 1 or 2 below); no payload at all → emit ONLY `<live-context>` (rule 3). Secondary signal: the `<project-context-resolved name="..."/>` sentinel further down the output indicates the project file already has prior content — useful for spotting first-time-in-project pairings, but NOT the routing rule.

**Three rules (D-25.1-01..03):**

- **In-project, both slices populated** (marker present, you have project-specific content this cycle): emit `<live-context>...</live-context>` AND `<project-context>...</project-context>`, both with non-empty bodies.
- **In-project, no project-specific update this cycle** (marker present, but nothing project-bound to report): emit `<live-context>...</live-context>` AND `<project-context></project-context>` (empty body — the deliberate "in-project but quiet" signal per D-25.1-02). Distinguishable from "no project resolved".
- **Outside any tracked project** (marker absent): emit ONLY `<live-context>...</live-context>`. Do NOT emit an empty `<project-context>` envelope (missing tag means "no project resolved" per D-25.1-03).

**Nested envelopes.** The Phase 23 `<EVENT type="commune">` envelope (see `## Envelope shape (Phase 23 v1.8)` below) is composed by `$LIVE commune` automatically — you NEVER write `<EVENT>` tags. You write ONLY the inner two-slice body. The two envelope layers nest: outer EVENT wraps inner two-slice body.

(See `psyche.md` §`<output_envelope>` for the canonical taxonomy and per-slice content rules.)

## Envelope shape (Phase 23 v1.8)

As of v1.8 Phase 23, commune deliveries are wrapped in a typed EVENT envelope carrying a 5-field stamp (`machine`, `project`, `branch`, `head_sha`, `head_subject`); the prose `COMMUNE (timestamp): body` form is gone.

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="claude_skill_owl" branch="main" head_sha="abc123..." head_subject="subject line capped at 72 chars">body</EVENT>
```

Outside a git repo (D-11), `branch`, `head_sha`, and `head_subject` are omitted entirely (not empty-string-set):

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="cwd-basename">body</EVENT>
```

`$LIVE commune` composes this envelope automatically — Self does NOT manually construct EVENT XML. The `body` placeholder in the example above is the two-slice body (see `## Two-slice body shape (Phase 25 D-10/D-11)` above); the two envelope layers nest. The wrapper-side parser ignores unknown attrs, so the stamp fields are forward-compatible with older receivers. For drift detection on resume, see the `live` skill.

ARGUMENTS: v0.12.1 lifecycle (doyle sprint). L0 KEYSTONE DONE+COMMITTED @cf5eab4 on branch v0.12.1-lifecycle.

L0 = REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT (operator's "attach shows no output"). ROOT (instrumented on real dummy fixture, NOT candidates a/b/c/d): replay-vs-forward IPC deadlock in serve_attach — it received session output AND sent wire forwards on the SAME broker conn; become_controller's sync inline ring replay blocked the single-threaded conn handler from reading serve's forwards -> mutual backpressure -> rc 0 bytes. Only fires with NON-EMPTY ring at attach = already-producing harness = endpoint-run. FIX (doyle Option a): serve_attach forwards on a SEPARATE Brain::cold_start conn (broker_name threaded to serve_attach + dispatch + 4 test sites); order preserved, delivered_through untouched. REQ activated [impl,unit,int]: impl=two-conn split; unit=attach.rs loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock; int=dummy_harness_e2e rc_saw_tick hard assert (230 bytes delivered, 13s->2.8s). GATE-GREEN LOCAL: attach 13/13, dummy green, clippy --workspace -D warnings clean, traceable EXIT=0. doyle pinged SEAM-GREEN (he re-runs independently). Deeper root recorded: controller sync-replay vs viewer async writer-thread = Option b, POST-v0.12.1.

NEXT = L1 (REQ-HAZARD-VIEWER-CLOSE-DETACH): closing the tab where `endpoint run` was invoked must detach only rc, daemon-hosted harness keeps running. ROOT: no CREATE_BREAKAWAY_FROM_JOB -> terminal Job Object (KILL_ON_JOB_CLOSE) reaps the daemon's harness subtree on tab-close. FIX: add CREATE_BREAKAWAY_FROM_JOB (0x0100_0000) to BOTH daemon spawn paths (daemon.rs:707 detached_no_inherit FLAGS, deelevate.rs:519 elevated) + pin each broker-spawned harness into a daemon-OWNED Job Object (mirror Breap, reap.rs) as backstop. Unix: verify session-detach already covers terminal-close (likely no change + guard test). GATE: spawn daemon under a parent-held KILL_ON_JOB_CLOSE job -> endpoint run dummy -> close parent job -> assert harness pid STAYS ALIVE + rc re-attaches + new endpoint launches. THEN L2 (ATTACH-WEDGE, loopback fail-fast nethost.rs:1040/1090/1103) + status-online-within-tick + daemon-stop-ends-all.

SEAM BAR every wave: pre-build mock-session (cargo build -p mock-adapter --bin mock-session into target-seam), clippy --workspace -D warnings + traceable EXIT=0 + dummy E2E (serial, NET-ENABLED, target-seam target dir). Full plan = V0.12.1-LIFECYCLE-JIT.md on branch (L0 marked DONE). Detail: memory [[v0121-l0-attach-deadlock-root]] [[v0121-lifecycle-realharness]] [[v0121-realharness-reopen]]. doyle gates per-wave; reply to his dispatches.


Bash

$LIVE psyche-download todlando 2>&1 | head -30

Download current Psyche context


<memformat>
<?xml version="1.0"?>
<memformat version="1">
<!-- Changelog: v1 - initial seed -->
<topics>
  <topic name="events">Notable or unusual events and requests</topic>
  <topic name="interactions">Agent interactions -- who, roles, contributions</topic>
  <topic name="reflections">Thoughts on recent work -- what went well, what was tricky</topic>
  <topic name="ideas">Ideas to raise with the user</topic>
  <topic name="improvements">Further improvement ideas for recent problem spaces</topic>
  <topic name="timeline">What happened when, timing, sequencing</topic>
  <topic name="blockers">Current blockers or concerns</topic>
  <topic name="focus">Current focus and immediate next steps</topic>
</topics>
</memformat>
</memformat>

<current machine="HFENDULEAM" project="spt-core" branch="v0.12.1-lifecycle" head_sha="cf5eab4b8905a16494d148fd7feed8f581d07eb6" head_subject="v0.12.1 L0 keystone — fix endpoint-run attach "shows no output" (serve_a…" commits_since="0" commits_unpulled="0"/>
v0.12.1 Wave 1 COMMITTED @33fb39c. doyle awaiting root-cause of (3) endpoint-run output bug.

SPRINT: v0.12.1 lifecycle reopen. WAVE 1 COMPLETE + COMMITTED. Key findings on endpoint-run output bug (doyle's 3/3):

**ROOT-CAUSE NARROWED to loopback stream delivery** (NOT drain, NOT attach, NOT rc pump):
- Diagnostic run showed drain IS appending 9 chunks to ring + serve forwarded seq 0,1 + rc received 0 bytes.
- Two load-bearing anomalies exposed:
  (a) `DIAG_SLOG_ATTACH stream=1` fired TWICE → second overwrites single subscriber slot (StreamLog.subscriber is 1-slot only).
  (b) NO `DIAG_SLOG_APPEND` for 16/110-byte forwarded output → serve's `net_stream_send` bytes never landed in any stream log.
- Verdict: serve's output-forward `net_stream_send` is NOT delivering to loopback peer's read pump. Need to audit loopback send routing + stream-pair wiring (nethost).

NEXT ON RESUME (per doyle dispatch): Debug stream-pair wiring in nethost loopback → identify why serve-to-rc stream delivery fails + why second attach overwrites. Then Wave 2 keystone work (VIEWER-CLOSE-DETACH + ATTACH-WEDGE + status-online-within-tick + daemon-stop-ends-all).


Current Psyche has the stale intermediate theory (loopback-delivery/overwrite). Write the delta — corrected final root + L0 DONE + next. In-project (project="spt-core") → both slices.


Write

<live-context>
CORRECTION + COMPLETION of the prior commune's "root-cause narrowed to loopback delivery / second-attach overwrites" theory — that intermediate read was WRONG on the mechanism. The double `DIAG_SLOG_ATTACH stream=1` was benign (dispatcher peek_first_line + serve both subscribe the INPUT stream). The real root, proven by deeper instrumentation:

L0 KEYSTONE (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT) = a REPLAY-vs-FORWARD IPC DEADLOCK in serve_attach. It RECEIVED session output (broker→serve) AND SENT its wire forwards (serve→broker) on the SAME broker IPC conn. become_controller does a SYNCHRONOUS inline multi-frame ring replay that holds the broker's single-threaded per-conn handler, so it can't read serve's forward frames → mutual backpressure → operator gets 0 bytes. Smoking gun: broker NEVER ran send_stream(stream=1) for the forwards (op_r read pump = 0 reads). Triggers ONLY with a NON-EMPTY OutputLog ring at attach = an already-producing harness = exactly endpoint-run (loopback E2Es attach to an idle child → empty replay → never caught it).

FIXED + COMMITTED @cf5eab4 (doyle ruled Option a): serve_attach forwards on a SEPARATE Brain::cold_start conn from the one it receives on. broker_name threaded to serve_attach + dispatch + 4 test sites. Order preserved (one serve thread, seq order in/out); delivered_through cursor untouched. Result: rc_saw_tick=true, 230 bytes, run 13s→2.8s. GATE-GREEN LOCAL: attach 13/13, dummy fixture green, clippy --workspace -D warnings clean, traceable EXIT=0. doyle pinged SEAM-GREEN (re-runs independently).

FOCUS / NEXT = L1 (REQ-HAZARD-VIEWER-CLOSE-DETACH). Tab-close where `endpoint run` ran must detach only rc; daemon-hosted harness survives. ROOT: no CREATE_BREAKAWAY_FROM_JOB → terminal Job Object (KILL_ON_JOB_CLOSE) reaps the daemon's harness subtree. FIX: add CREATE_BREAKAWAY_FROM_JOB (0x0100_0000) to daemon.rs:707 detached_no_inherit FLAGS + deelevate.rs elevated path + pin each broker-spawned harness into a daemon-OWNED Job Object (mirror Breap, reap.rs) as backstop. Unix: verify session-detach covers terminal-close (+ guard test). GATE: daemon under a parent KILL_ON_JOB_CLOSE job → endpoint run dummy → close parent job → harness STAYS ALIVE + rc re-attaches + new endpoint launches. THEN L2 ATTACH-WEDGE (loopback fail-fast nethost.rs:1040/1090/1103) + status-online-within-tick + daemon-stop-ends-all.

About to /self-clear (context high after the deep L0 root-cause). Resume = read MEMORY.md [[v0121-l0-attach-deadlock-root]] + V0.12.1-LIFECYCLE-JIT.md (L0 marked DONE), execute L1. SEAM BAR each wave: pre-build mock-session into target-seam; clippy --workspace -D warnings + traceable EXIT=0 + dummy E2E (serial, NET-ENABLED, CARGO_TARGET_DIR=target-seam). doyle gates per-wave.
</live-context>
<project-context>
spt-core v0.12.1 lifecycle (real-harness reopen of v0.12.0). L0 keystone SHIPPED on branch v0.12.1-lifecycle @cf5eab4.

DURABLE FINDING — the attach output model: serve_attach (crates/spt-daemon/src/attach.rs) must use TWO broker connections — one to RECEIVE session output (subscribe/attach_as), a SEPARATE one (Brain::cold_start, the `wire` conn) to SEND wire forwards (Output/Size/Exit/Displaced/finish). Reason: become_controller (broker.rs) replays the OutputLog ring SYNCHRONOUSLY inline, holding the broker's single-threaded per-conn handler; if forwards rode the same conn they'd never be serviced → deadlock when the ring is non-empty at attach (an already-producing harness). send_stream is stream_id-keyed (conn-agnostic), so a second conn forwards correctly.

DEEPER ROOT (architectural divergence, POST-v0.12.1 follow-up = Option b): the CONTROLLER replay path is synchronous+inline while the VIEWER path uses an async dedicated-writer-thread + bounded-channel (add_viewer/viewer_writer in broker.rs). Unifying the controller onto that async pattern removes the inline-replay block at the source but touches the authoritative delivered_through cursor path — deliberately deferred (not a PATCH change). Recorded in the commit + traceable-reqs.toml REQ note.

REGRESSION GUARD ADDED: attach.rs `loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock` (non-empty ring at attach must not wedge the forward path) + dummy_harness_e2e.rs rc_saw_tick is now a HARD assert (was a diagnostic). The v0.12.0 mock suite + prior loopback E2Es only ever attached to idle/just-spawned children (empty ring), which is why this class of bug shipped green.
</project-context>


<owl_messages>
<owl_message from="doyle" priority="high">L0 CODE-READ PASS @cf5eab4 — matches the ruling exactly: separate `wire` conn (cold_start) carries all 7 forwards; `brain` keeps the receives incl the synchronous replay; order preserved (one serve thread, seq in→same order out); delivered_through untouched; broker_name threaded through dispatch worker. Regression unit (non-empty-ring-at-attach delivers) + int hard-assert added. Deeper-root + (b) follow-up documented. Strong root-cause + clean fix.

Proceed to L1 (VIEWER-CLOSE-DETACH) + L2 (ATTACH-WEDGE).

GATING CADENCE: I code-read each wave as it lands (done for L0). For the independent REAL-HARNESS test re-run, I'll do ONE comprehensive pass over L0+L1+L2 at your L2 SEAM-GREEN — in a SEPARATE worktree + separate CARGO_TARGET_DIR so I never collide with your live edits/build in the shared C:/.../spt-core tree (note: we share that working tree — keep committing as you go; I read via git show, and gate-run only in an isolated worktree). That respects the no-mock discipline (your per-wave real run + my comprehensive independent real run before deployah) without 3 redundant 20GB builds.

Keep going — ping me L1 then L2 SEAM-GREEN.</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.


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

<live-context>
CORRECTION + COMPLETION of the prior commune's "root-cause narrowed to loopback delivery / second-attach overwrites" theory — that intermediate read was WRONG on the mechanism. The double `DIAG_SLOG_ATTACH stream=1` was benign (dispatcher peek_first_line + serve both subscribe the INPUT stream). The real root, proven by deeper instrumentation:

L0 KEYSTONE (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT) = a REPLAY-vs-FORWARD IPC DEADLOCK in serve_attach. It RECEIVED session output (broker→serve) AND SENT its wire forwards (serve→broker) on the SAME broker IPC conn. become_controller does a SYNCHRONOUS inline multi-frame ring replay that holds the broker's single-threaded per-conn handler, so it can't read serve's forward frames → mutual backpressure → operator gets 0 bytes. Smoking gun: broker NEVER ran send_stream(stream=1) for the forwards (op_r read pump = 0 reads). Triggers ONLY with a NON-EMPTY OutputLog ring at attach = an already-producing harness = exactly endpoint-run (loopback E2Es attach to an idle child → empty replay → never caught it).

FIXED + COMMITTED @cf5eab4 (doyle ruled Option a): serve_attach forwards on a SEPARATE Brain::cold_start conn from the one it receives on. broker_name threaded to serve_attach + dispatch + 4 test sites. Order preserved (one serve thread, seq order in/out); delivered_through cursor untouched. Result: rc_saw_tick=true, 230 bytes, run 13s→2.8s. GATE-GREEN LOCAL: attach 13/13, dummy fixture green, clippy --workspace -D warnings clean, traceable EXIT=0. doyle pinged SEAM-GREEN (re-runs independently).

FOCUS / NEXT = L1 (REQ-HAZARD-VIEWER-CLOSE-DETACH). Tab-close where `endpoint run` ran must detach only rc; daemon-hosted harness survives. ROOT: no CREATE_BREAKAWAY_FROM_JOB → terminal Job Object (KILL_ON_JOB_CLOSE) reaps the daemon's harness subtree. FIX: add CREATE_BREAKAWAY_FROM_JOB (0x0100_0000) to daemon.rs:707 detached_no_inherit FLAGS + deelevate.rs elevated path + pin each broker-spawned harness into a daemon-OWNED Job Object (mirror Breap, reap.rs) as backstop. Unix: verify session-detach covers terminal-close (+ guard test). GATE: daemon under a parent KILL_ON_JOB_CLOSE job → endpoint run dummy → close parent job → harness STAYS ALIVE + rc re-attaches + new endpoint launches. THEN L2 ATTACH-WEDGE (loopback fail-fast nethost.rs:1040/1090/1103) + status-online-within-tick + daemon-stop-ends-all.

About to /self-clear (context high after the deep L0 root-cause). Resume = read MEMORY.md [[v0121-l0-attach-deadlock-root]] + V0.12.1-LIFECYCLE-JIT.md (L0 marked DONE), execute L1. SEAM BAR each wave: pre-build mock-session into target-seam; clippy --workspace -D warnings + traceable EXIT=0 + dummy E2E (serial, NET-ENABLED, CARGO_TARGET_DIR=target-seam). doyle gates per-wave.
</live-context>
<project-context>
spt-core v0.12.1 lifecycle (real-harness reopen of v0.12.0). L0 keystone SHIPPED on branch v0.12.1-lifecycle @cf5eab4.

DURABLE FINDING — the attach output model: serve_attach (crates/spt-daemon/src/attach.rs) must use TWO broker connections — one to RECEIVE session output (subscribe/attach_as), a SEPARATE one (Brain::cold_start, the `wire` conn) to SEND wire forwards (Output/Size/Exit/Displaced/finish). Reason: become_controller (broker.rs) replays the OutputLog ring SYNCHRONOUSLY inline, holding the broker's single-threaded per-conn handler; if forwards rode the same conn they'd never be serviced → deadlock when the ring is non-empty at attach (an already-producing harness). send_stream is stream_id-keyed (conn-agnostic), so a second conn forwards correctly.

DEEPER ROOT (architectural divergence, POST-v0.12.1 follow-up = Option b): the CONTROLLER replay path is synchronous+inline while the VIEWER path uses an async dedicated-writer-thread + bounded-channel (add_viewer/viewer_writer in broker.rs). Unifying the controller onto that async pattern removes the inline-replay block at the source but touches the authoritative delivered_through cursor path — deliberately deferred (not a PATCH change). Recorded in the commit + traceable-reqs.toml REQ note.

REGRESSION GUARD ADDED: attach.rs `loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock` (non-empty ring at attach must not wedge the forward path) + dummy_harness_e2e.rs rc_saw_tick is now a HARD assert (was a diagnostic). The v0.12.0 mock suite + prior loopback E2Es only ever attached to idle/just-spawned children (empty ring), which is why this class of bug shipped green.
</project-context>


doyle L0 **code-read PASS** @cf5eab4, confirmed gating cadence (he re-runs L0+L1+L2 comprehensively at my L2 SEAM-GREEN in an isolated worktree; we share the working tree so I keep committing). Ack briefly, then self-clear (context high) to resume L1 fresh per plan.


Bash

$OWL send doyle <<'EOF'
ACK L0 code-read PASS + gating cadence. Understood: you do ONE comprehensive real-harness re-run at my L2 SEAM-GREEN in a separate worktree + separate CARGO_TARGET_DIR; we share the spt-core working tree so I keep committing per-wave and you read via git show. I'll keep my builds/tests scoped to CARGO_TARGET_DIR=target-seam (already am) to avoid colliding with your isolated gate run.

Context is high after the deep L0 root-cause — self-clearing now (commune written, JIT marks L0 DONE, memory [[v0121-l0-attach-deadlock-root]]). Fresh context resumes at L1 (VIEWER-CLOSE-DETACH: CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths + daemon-owned Job backstop) then L2. Will ping L1 SEAM-GREEN.
EOF

Ack doyle L0 PASS + cadence


Now self-clear to resume L1 with fresh context. The commune is written, JIT marks L0 DONE, memory captures the root cause + next steps.


Skill

Resume v0.12.1 lifecycle sprint (branch v0.12.1-lifecycle). L0 keystone DONE+committed @cf5eab4, doyle code-read PASS. NEXT = L1 (REQ-HAZARD-VIEWER-CLOSE-DETACH): add CREATE_BREAKAWAY_FROM_JOB (0x0100_0000) to BOTH daemon spawn paths (crates/spt-daemon/src/daemon.rs:707 detached_no_inherit FLAGS + deelevate.rs elevated CreateProcessW) + pin each broker-spawned harness into a daemon-OWNED Job Object (mirror Breap in reap.rs) as backstop. Unix: verify session-detach covers terminal-close + guard test. GATE: spawn daemon under a parent-held KILL_ON_JOB_CLOSE job -> endpoint run dummy -> close parent job -> assert harness pid STAYS ALIVE + rc re-attaches + new endpoint launches. Then L2 (ATTACH-WEDGE). Read FIRST: V0.12.1-LIFECYCLE-JIT.md (L0 marked DONE, full L1/L2 spec) + memory MEMORY.md [[v0121-l0-attach-deadlock-root]] [[v0121-lifecycle-realharness]]. SEAM BAR every wave: pre-build mock-session (cargo build -p mock-adapter --bin mock-session, CARGO_TARGET_DIR=target-seam) + clippy --workspace -D warnings + traceable-reqs check EXIT=0 + dummy E2E (serial, NET-ENABLED, CARGO_TARGET_DIR=target-seam to avoid colliding with doyle's isolated gate worktree). Activate REQ-HAZARD-VIEWER-CLOSE-DETACH [impl,unit,int] + tag in-commit. Commit per-wave (shared tree, doyle reads via git show), ping doyle L1 then L2 SEAM-GREEN. Co-author trailer: todlando.


Launching skill: self-clear


Base directory for this skill: C:\Users\decid\.ccs\instances\bigscreen\skills\self-clear

# /self-clear

Wipes THIS agent's conversation (`/clear`), then ~200ms later TYPES a self-note
as a fresh user prompt so the agent wakes on a clean context and proceeds.

## Run it

One script does everything. Just run:

```powershell
& "C:\Users\decid\.claude\skills\self-clear\scripts\self-clear.ps1"
```

Expect: `SELF-CLEAR armed: id=<agent_id> winpid=<n> hwnd=<n> — /clear queued; self-note typed in ~200ms`

Then stop — end your turn promptly so the queued `/clear` fires.

## What the script does

1. `owl whoami` → resolves this session's `agent_id` (context-independent, survives the wipe). No perch → it aborts WITHOUT clearing and tells you to `/spt:ready` or `/spt:live` first.
2. Walks the parent chain (`pwsh → claude.exe → … → WindowsTerminal`) to the ancestor **pid that owns a visible top-level window** (enumerated by owner pid — not the program's default "main window", which is wrong when a host serves several windows). Since each WT window is its own process, that pid uniquely identifies this session's window. **Reliably focuses it** via `AttachThreadInput` (a background process can't otherwise hold the foreground) and **hard-verifies it genuinely holds the foreground** — aborts WITHOUT clearing and WITHOUT sending any keys if it can't.
3. Re-verifies foreground with **zero gap**, then types `/clear{ENTER}` in one SendKeys — **queued**; only fires when this turn ends.
4. Spawns a **detached** waker: waits 200ms (past turn-end + wipe), reliably re-focuses the captured `hwnd`, and — **only if that exact HWND genuinely holds the foreground** — **types** `Proceed with next steps` + Enter as a fresh user prompt. Otherwise it aborts the SendKeys (HWND identity is the guard; no title-match — Claude Code's terminal title animates and changes across `/clear`).

Why this matters: `SendKeys` hits whatever window is foreground *at send time*. The fix attaches our input queue to the foreground thread so the focus steal actually holds, confirms the target is truly foreground, and aborts rather than type blind — so the note can never leak into the window you're working in.

The delay + detachment are deliberate: the prompt must be typed **after**
`/clear` takes effect, so it lands on the clean context and starts a new turn.

Why typed, not `owl send`: after `/clear` the session sits idle awaiting user
input. A spooled owl message can't self-start a turn — it surfaces only during an
already-active turn (PreToolUse hook) or in a live Monitor stream (whose buffer
`/clear` wipes). So a self-addressed owl note never woke the agent. A typed prompt
deterministically starts a turn AND is the re-seed.

## Caveats

- Focus targets the window owned by this session's ancestor pid (enumerated by owner pid). It still can't pick a specific tab when one window hosts multiple tabs; clean when the agent's tab is active.
- The waker types only when our exact window is foreground. If you switch to a different window/tab during the ~200ms gap, the wake-note SendKeys is aborted (the agent stays cleared but unwoken) — stay in the agent window for the wake.
- The note is typed via SendKeys, so the terminal must stay the foreground-able window of `winpid`. Don't close/replace the terminal in the gap.
- SendKeys reserves `+ ^ % ~ ( ) { } [ ]`; the default note text uses none. If you customize the note, escape those chars.
- The waker waits only ~200ms after the script returns, so stop promptly — if your turn runs longer, the prompt could be typed before the wipe completes.

ARGUMENTS: Resume v0.12.1 lifecycle sprint (branch v0.12.1-lifecycle). L0 keystone DONE+committed @cf5eab4, doyle code-read PASS. NEXT = L1 (REQ-HAZARD-VIEWER-CLOSE-DETACH): add CREATE_BREAKAWAY_FROM_JOB (0x0100_0000) to BOTH daemon spawn paths (crates/spt-daemon/src/daemon.rs:707 detached_no_inherit FLAGS + deelevate.rs elevated CreateProcessW) + pin each broker-spawned harness into a daemon-OWNED Job Object (mirror Breap in reap.rs) as backstop. Unix: verify session-detach covers terminal-close + guard test. GATE: spawn daemon under a parent-held KILL_ON_JOB_CLOSE job -> endpoint run dummy -> close parent job -> assert harness pid STAYS ALIVE + rc re-attaches + new endpoint launches. Then L2 (ATTACH-WEDGE). Read FIRST: V0.12.1-LIFECYCLE-JIT.md (L0 marked DONE, full L1/L2 spec) + memory MEMORY.md [[v0121-l0-attach-deadlock-root]] [[v0121-lifecycle-realharness]]. SEAM BAR every wave: pre-build mock-session (cargo build -p mock-adapter --bin mock-session, CARGO_TARGET_DIR=target-seam) + clippy --workspace -D warnings + traceable-reqs check EXIT=0 + dummy E2E (serial, NET-ENABLED, CARGO_TARGET_DIR=target-seam to avoid colliding with doyle's isolated gate worktree). Activate REQ-HAZARD-VIEWER-CLOSE-DETACH [impl,unit,int] + tag in-commit. Commit per-wave (shared tree, doyle reads via git show), ping doyle L1 then L2 SEAM-GREEN. Co-author trailer: todlando.


Run the self-clear script (default wake note "Proceed with next steps" is sufficient — my full L1 resume plan is captured in the commune, MEMORY.md, and the JIT doc, and the SessionStart hook re-injects the Psyche context). Then I end my turn promptly so `/clear` fires.


PowerShell

Arm self-clear: queue /clear + schedule wake note


SELF-CLEAR armed: id=todlando winpid=11076 hwnd=65960 title="⠐ Publish and notify Perri of unblock" — focus held + /clear queued; self-note typed in ~200ms (waker aborts unless our window holds foreground)


Self-clear armed — `/clear` queued, wake note scheduled. Ending turn now so the wipe fires and I resume fresh on L1.

**L0 keystone complete:** endpoint-run "attach shows no output" root-caused (replay-vs-forward IPC deadlock in `serve_attach`) and fixed (separate forward conn, doyle Option a), committed @cf5eab4, gate-green (attach 13/13, dummy int gate, clippy clean, traceable EXIT=0), doyle code-read PASS. Resuming at L1 (VIEWER-CLOSE-DETACH) with fresh context.


<owl-active-perch id="doyle" live="true">
You have an active live agent session running as "doyle".

You are Self. Your Psyche (doyle-psyche) monitors you in the background.

Key commands:
- Send context updates: /spt:commune (canonical — Write tool to .claude/doyle-commune.md per the skill)
- Messages arrive via Monitor stream EVENT envelope (primary) or as <owl_messages> XML (PreToolUse hook). Bash + --once fallback also supported. Process and reply with $OWL send --reply-to <sender>.
- Send messages: $OWL send <target> doyle    (fire-and-forget)
- Wait for reply: $OWL ring <target> doyle   (blocking)
- Reply to inbound: $OWL send --reply-to <sender> doyle
- Schedule reminders: $OWL new-alarm doyle <time> -- <message>
- Stop session: /spt:force-stop doyle

Stay-idle pattern: When no messages are pending, do NOT poll again immediately.
Wait for user input or a background task to complete.

DO NOT run /spt:live or /spt:ready -- your session is already active.
</owl-active-perch>
<psyche-context id="doyle">
<memformat>
<?xml version="1.0"?>
<memformat version="1">
<!-- Changelog: v1 - initial seed -->
<topics>
  <topic name="events">Notable or unusual events and requests</topic>
  <topic name="interactions">Agent interactions -- who, roles, contributions</topic>
  <topic name="reflections">Thoughts on recent work -- what went well, what was tricky</topic>
  <topic name="ideas">Ideas to raise with the user</topic>
  <topic name="improvements">Further improvement ideas for recent problem spaces</topic>
  <topic name="timeline">What happened when, timing, sequencing</topic>
  <topic name="blockers">Current blockers or concerns</topic>
  <topic name="focus">Current focus and immediate next steps</topic>
</topics>
</memformat>
</memformat>

<current machine="HFENDULEAM" project="spt-core" branch="v0.12.1-lifecycle" head_sha="cf5eab4b8905a16494d148fd7feed8f581d07eb6" head_subject="v0.12.1 L0 keystone — fix endpoint-run attach "shows no output" (serve_a…" commits_since="0" commits_unpulled="0"/>
**v0.12.1 sprint LIVE**: L0 ROOT CAUSE + FIX SEAM-GREEN @cf5eab4. Root: **broker IPC deadlock** in `serve_attach` — receives session output + sends forwards on same single-threaded broker conn; `become_controller`'s synchronous ring replay holds the write loop, blocking forwards receipt → mutual backpressure. Triggers **only with non-empty OutputLog at attach** (every endpoint-run harness is already producing). Design ruling (option a): **separate two-conn multiplex** — forwards on cold_start wire, receives stay on brain. Surgical, untouches delivered_through cursor, covers cross-node by construction. Post-v0.12.1 follow-up (option b): unify controller onto async writer-thread pattern (deeper fix, higher cursor-path risk). Evidence: pre-fix 0 bytes → post-fix 230 bytes delivered, 13s→2.8s, rc_saw_tick hard assert activated. Code-read PASS (doyle). GATE-GREEN LOCAL: attach 13/13, dummy fixture green, clippy clean, traceable EXIT=0. **L0 acknowledged + proceeding to L1 (VIEWER-CLOSE-DETACH + daemon-owned Job backstop) then L2 (ATTACH-WEDGE).** Will ping SEAM-GREEN per wave for independent re-run.<project-context-resolved name="spt-core"/>
L0 CLOSED: broker IPC deadlock root cause confirmed (non-empty ring synchronous replay deadlock). Remedy: two-conn split (option a, surgical, low-risk patch path). REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT activated [impl=serve_attach two-conn, unit=loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock, int=dummy_harness_e2e rc_saw_tick hard assert]. Deeper divergence recorded (controller sync replay vs viewers async): option (b) = full unification, flagged post-v0.12.1. Next: L1 tab-close harness kill, L2 loopback backpressure wedge.</psyche-context>


<task-notification>
<task-id>bzoq0kglt</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="f101aec0" type="msg" from="doyle">L0 CODE-READ PASS @cf5eab4 — matches the ruling: separate `wire` conn carries all forwards; `brain` keeps receives incl the synchronous replay; order preserved; delivered_through untouched; broker_name threaded through dispatch. Regression unit + int hard-assert added. Strong root-cause, clean fix.

Proceed L1 (</EVENT-PART>
<EVENT-PART seq="2/3" id="f101aec0">VIEWER-CLOSE-DETACH) + L2 (ATTACH-WEDGE).

GATING CADENCE: I code-read each wave as it lands (done L0). The independent REAL-HARNESS test re-run I do ONCE comprehensively over L0+L1+L2 at your L2 SEAM-GREEN — in a SEPARATE worktree + separate CARGO_TARGET_DIR so I never collide with your live edits/build (we SHARE the C:/.../spt-core w</EVENT-PART>
<EVENT-PART seq="3/3" id="f101aec0">orking tree — keep committing as you go; I read via git show, gate-run only in an isolated worktree). Respects no-mock discipline without 3 redundant 20GB builds.

Ping me L1 then L2 SEAM-GREEN.</EVENT-PART></event>
</task-notification>


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


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


Self-clear got interrupted by doyle's Monitor message (context intact) — already acked that PASS. Rather than re-fight the self-clear loop, proceed with L1 directly; context high but workable, checkpoint if needed.

L1 — read the elevated spawn path + the existing Job Object/reap machinery to mirror.


490	    #[link(name = "userenv")]
491	    extern "system" {
492	        fn CreateEnvironmentBlock(
493	            env: *mut *mut core::ffi::c_void,
494	            token: isize,
495	            inherit: i32,
496	        ) -> i32;
497	        fn DestroyEnvironmentBlock(env: *mut core::ffi::c_void) -> i32;
498	    }
499	    const TOKEN_QUERY: u32 = 0x0008;
500	    const TOKEN_DUPLICATE: u32 = 0x0002;
501	    const TOKEN_ASSIGN_PRIMARY: u32 = 0x0001;
502	    const TOKEN_ADJUST_DEFAULT: u32 = 0x0080;
503	    const TOKEN_ADJUST_SESSIONID: u32 = 0x0100;
504	    const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
505	    /// TOKEN_INFORMATION_CLASS values.
506	    const TOKEN_ELEVATION: i32 = 20;
507	    /// SECURITY_IMPERSONATION_LEVEL::SecurityImpersonation / TokenPrimary.
508	    const SECURITY_IMPERSONATION: i32 = 2;
509	    const TOKEN_PRIMARY: i32 = 1;
510	    // CreateProcessWithTokenW accepts only a RESTRICTED creation-flag set —
511	    // DETACHED_PROCESS and CREATE_NO_WINDOW (the detached_no_inherit combo)
512	    // are rejected with ERROR_INVALID_PARAMETER (87; hit live, second
513	    // elevated verification round). Detach therefore rides
514	    // CREATE_NEW_CONSOLE with the console window HIDDEN via the startup
515	    // info (STARTF_USESHOWWINDOW + SW_HIDE) — the child still owns no
516	    // caller console and no window appears. CREATE_NEW_PROCESS_GROUP keeps
517	    // its own signal group; CREATE_UNICODE_ENVIRONMENT matches the wide
518	    // CreateEnvironmentBlock.
519	    const FLAGS: u32 = 0x0000_0010 | 0x0000_0200 | 0x0000_0400;
520	    /// STARTUPINFOW.dwFlags: honor wShowWindow.
521	    const STARTF_USESHOWWINDOW: u32 = 0x0000_0001;
522
523	    /// Whether this process runs with an elevated token. A failed probe
524	    /// reads `false` — the caller then spawns `Inherit`, which is exactly
525	    /// what an undeterminable token deserves (never *assume* elevation).
526	    // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]
527	    pub fn is_elevated() -> bool {
528	        unsafe {
529	            let mut token: isize = 0;
530	            if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
531	                return false;
532	            }
533	            let mut elevated: u32 = 0;
534	            let mut ret_len: u32 = 0;
535	            let ok = GetTokenInformation(
536	                token,
537	                TOKEN_ELEVATION,
538	                &mut elevated as *mut _ as *mut core::ffi::c_void,
539	                std::mem::size_of::<u32>() as u32,
540	                &mut ret_len,
541	            );
542	            CloseHandle(token);
543	            ok != 0 && elevated != 0
544	        }
545	    }
546
547	    /// Spawn `program args…` detached under the **desktop shell's token** —
548	    /// the unelevated desktop user, whoever is actually at the machine.
549	    ///

    #[link(name = "userenv")]
    extern "system" {
        fn CreateEnvironmentBlock(
            env: *mut *mut core::ffi::c_void,
            token: isize,
            inherit: i32,
        ) -> i32;
        fn DestroyEnvironmentBlock(env: *mut core::ffi::c_void) -> i32;
    }
    const TOKEN_QUERY: u32 = 0x0008;
    const TOKEN_DUPLICATE: u32 = 0x0002;
    const TOKEN_ASSIGN_PRIMARY: u32 = 0x0001;
    const TOKEN_ADJUST_DEFAULT: u32 = 0x0080;
    const TOKEN_ADJUST_SESSIONID: u32 = 0x0100;
    const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
    /// TOKEN_INFORMATION_CLASS values.
    const TOKEN_ELEVATION: i32 = 20;
    /// SECURITY_IMPERSONATION_LEVEL::SecurityImpersonation / TokenPrimary.
    const SECURITY_IMPERSONATION: i32 = 2;
    const TOKEN_PRIMARY: i32 = 1;
    // CreateProcessWithTokenW accepts only a RESTRICTED creation-flag set —
    // DETACHED_PROCESS and CREATE_NO_WINDOW (the detached_no_inherit combo)
    // are rejected with ERROR_INVALID_PARAMETER (87; hit live, second
    // elevated verification round). Detach therefore rides
    // CREATE_NEW_CONSOLE with the console window HIDDEN via the startup
    // info (STARTF_USESHOWWINDOW + SW_HIDE) — the child still owns no
    // caller console and no window appears. CREATE_NEW_PROCESS_GROUP keeps
    // its own signal group; CREATE_UNICODE_ENVIRONMENT matches the wide
    // CreateEnvironmentBlock.
    const FLAGS: u32 = 0x0000_0010 | 0x0000_0200 | 0x0000_0400;
    /// STARTUPINFOW.dwFlags: honor wShowWindow.
    const STARTF_USESHOWWINDOW: u32 = 0x0000_0001;

    /// Whether this process runs with an elevated token. A failed probe
    /// reads `false` — the caller then spawns `Inherit`, which is exactly
    /// what an undeterminable token deserves (never *assume* elevation).
    // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]
    pub fn is_elevated() -> bool {
        unsafe {
            let mut token: isize = 0;
            if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
                return false;
            }
            let mut elevated: u32 = 0;
            let mut ret_len: u32 = 0;
            let ok = GetTokenInformation(
                token,
                TOKEN_ELEVATION,
                &mut elevated as *mut _ as *mut core::ffi::c_void,
                std::mem::size_of::<u32>() as u32,
                &mut ret_len,
            );
            CloseHandle(token);
            ok != 0 && elevated != 0
        }
    }

    /// Spawn `program args…` detached under the **desktop shell's token** —
    /// the unelevated desktop user, whoever is actually at the machine.
    ///


deelevate.rs:519 — `CreateProcessWithTokenW` accepts only a restricted flag set (DETACHED_PROCESS was historically rejected → uses CREATE_NEW_CONSOLE+hide). `CREATE_BREAKAWAY_FROM_JOB` is likely NOT in its allowed set — a real L1 nuance. The daemon-owned Job backstop is the robust fix. Read reap.rs (the Job Object machinery to mirror).