980	                    }
981	                    std::thread::sleep(Duration::from_millis(20));
982	                }
983	            };
984	            assert!(dead, "stop_host must terminate the detached psyche process");
985	        });
986	    }
987
988	    // [unit->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP] the ID-SPECIFIC orphan-reap guard:
989	    // the real `{id}` psyche (matching exe basename AND a cmdline carrying
990	    // "<id>-psyche") is reapable, but a SIBLING with the SAME basename and a DIFFERENT
991	    // id is SPARED — the shared-box recycle/sibling safety (basename alone would
992	    // wrong-kill a sibling `claude`). Only the decision is tested; nothing real-world
993	    // is reaped. Both children carry their id marker in argv (the {id} bake analog).
994	    #[test]
995	    fn orphan_reap_is_id_specific_spares_a_same_basename_sibling() {
996	        // Long-lived children with the SAME exe basename but DIFFERENT id markers.
997	        #[cfg(windows)]
998	        let spawn = |marker: &str| {
999	            std::process::Command::new("cmd")
1000	                .args(["/C", &format!("ping -n 30 127.0.0.1 >NUL & rem {marker}")])
1001	                .spawn()
1002	                .expect("spawn")
1003	        };
1004	        #[cfg(unix)]
1005	        let spawn = |marker: &str| {
1006	            std::process::Command::new("sh")
1007	                .args(["-c", &format!("sleep 30 #{marker}")])
1008	                .spawn()
1009	                .expect("spawn")
1010	        };
1011	        let mut psyche = spawn("agent7-psyche"); // the real {id} psyche
1012	        let mut sibling = spawn("agent7-other-psyche"); // a sibling agent, same basename
1013	        let prog = psyche_program_norm(if cfg!(windows) { "cmd" } else { "sh" }).unwrap();
1014
1015	        // Wait until the real psyche's cmdline is queryable + carries its marker.
1016	        let ready = {
1017	            let deadline = Instant::now() + Duration::from_secs(5);
1018	            loop {
1019	                if spt_store::proc::process_cmdline(psyche.id())
1020	                    .map(|c| c.contains("agent7-psyche"))
1021	                    .unwrap_or(false)
1022	                {
1023	                    break true;
1024	                }
1025	                if Instant::now() >= deadline {
1026	                    break false;
1027	                }
1028	                std::thread::sleep(Duration::from_millis(20));
1029	            }
1030	        };
1031	        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());
1032	        // The sibling: same basename, but its cmdline carries "agent7-other-psyche",
1033	        // NOT "agent7-psyche" → the id-specific gate spares it.
1034	        let spared = !psyche_orphan_should_reap("agent7-psyche", &prog, sibling.id());
1035
1036	        let _ = psyche.kill();
1037	        let _ = psyche.wait();
1038	        let _ = sibling.kill();
1039	        let _ = sibling.wait();
1040
1041	        assert!(real, "the real {{id}} psyche (basename + cmdline match) is reapable");
1042	        assert!(
1043	            spared,
1044	            "a same-basename sibling with a DIFFERENT id is SPARED — no wrong-kill on the shared box"
1045	        );
1046	    }
1047
1048	    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE] real broker session lifecycle:
1049	    // an spt-hosted perch with a LIVE broker session stays online across a pull
1050	    // reconcile; when its broker session is KILLED + reaped, the next pull
1051	    // reconcile clears the status=online latch and marks the perch offline.
1052	    #[test]
1053	    fn pull_reconcile_offlines_perch_when_broker_session_dies() {
1054	        use crate::msg::SpawnReq;

                    }
                    std::thread::sleep(Duration::from_millis(20));
                }
            };
            assert!(dead, "stop_host must terminate the detached psyche process");
        });
    }

    // [unit->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP] the ID-SPECIFIC orphan-reap guard:
    // the real `{id}` psyche (matching exe basename AND a cmdline carrying
    // "<id>-psyche") is reapable, but a SIBLING with the SAME basename and a DIFFERENT
    // id is SPARED — the shared-box recycle/sibling safety (basename alone would
    // wrong-kill a sibling `claude`). Only the decision is tested; nothing real-world
    // is reaped. Both children carry their id marker in argv (the {id} bake analog).
    #[test]
    fn orphan_reap_is_id_specific_spares_a_same_basename_sibling() {
        // Long-lived children with the SAME exe basename but DIFFERENT id markers.
        #[cfg(windows)]
        let spawn = |marker: &str| {
            std::process::Command::new("cmd")
                .args(["/C", &format!("ping -n 30 127.0.0.1 >NUL & rem {marker}")])
                .spawn()
                .expect("spawn")
        };
        #[cfg(unix)]
        let spawn = |marker: &str| {
            std::process::Command::new("sh")
                .args(["-c", &format!("sleep 30 #{marker}")])
                .spawn()
                .expect("spawn")
        };
        let mut psyche = spawn("agent7-psyche"); // the real {id} psyche
        let mut sibling = spawn("agent7-other-psyche"); // a sibling agent, same basename
        let prog = psyche_program_norm(if cfg!(windows) { "cmd" } else { "sh" }).unwrap();

        // Wait until the real psyche's cmdline is queryable + carries its marker.
        let ready = {
            let deadline = Instant::now() + Duration::from_secs(5);
            loop {
                if spt_store::proc::process_cmdline(psyche.id())
                    .map(|c| c.contains("agent7-psyche"))
                    .unwrap_or(false)
                {
                    break true;
                }
                if Instant::now() >= deadline {
                    break false;
                }
                std::thread::sleep(Duration::from_millis(20));
            }
        };
        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());
        // The sibling: same basename, but its cmdline carries "agent7-other-psyche",
        // NOT "agent7-psyche" → the id-specific gate spares it.
        let spared = !psyche_orphan_should_reap("agent7-psyche", &prog, sibling.id());

        let _ = psyche.kill();
        let _ = psyche.wait();
        let _ = sibling.kill();
        let _ = sibling.wait();

        assert!(real, "the real {{id}} psyche (basename + cmdline match) is reapable");
        assert!(
            spared,
            "a same-basename sibling with a DIFFERENT id is SPARED — no wrong-kill on the shared box"
        );
    }

    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE] real broker session lifecycle:
    // an spt-hosted perch with a LIVE broker session stays online across a pull
    // reconcile; when its broker session is KILLED + reaped, the next pull
    // reconcile clears the status=online latch and marks the perch offline.
    #[test]
    fn pull_reconcile_offlines_perch_when_broker_session_dies() {
        use crate::msg::SpawnReq;


doyle parts 9-10: NO production change (test-fixture only), commit on v0.12.0-lifecycle → push PR #25, run Windows seam bar first, bar = PR #25 GREEN both runners, ping SEAM-GREEN+CI-green-both → he re-gates → deployah resumes release (counter 25). ACK + verify helper signatures before editing.


Bash

cd /c/Users/decid/Documents/projects/spt-core
$OWL send doyle <<'EOF'
ACK full (parts 9-10 received). Plan: test-only fix on v0.12.0-lifecycle → push PR#25. FIX1 derive expected prog from psyche's live exe_basename after readiness poll; FIX2 harden unix marker `sh -c 'sleep 30; : <marker>'` in orphan_reap unit + proc.rs marker unit; Windows spawn unchanged; ZERO production change. Local Windows bar (changed units + clippy --workspace + traceable) then push, then ping you SEAM-GREEN + CI-green-both-runners. Building.
EOF
echo "=== helper signatures ==="
grep -rn "fn normalize_basename\|pub fn exe_basename\|fn psyche_program_norm\|pub fn process_cmdline" crates/ | grep -v "test"

ACK doyle + verify helper fn signatures/paths


➜ SENT:doyle
=== helper signatures ===
crates/spt-daemon/src/livehost.rs:559:fn psyche_program_norm(command: &str) -> Option<String> {
crates/spt-runtime/src/resolve.rs:55:pub fn normalize_basename(s: &str) -> String {
crates/spt-store/src/proc.rs:135:pub fn process_cmdline(pid: u32) -> Option<String> {
crates/spt-store/src/proc.rs:307:pub fn exe_basename(pid: u32) -> Option<String> {


Helpers confirmed. Read `psyche_orphan_should_reap` + `psyche_program_norm` to ensure the derived prog matches what the guard compares.


555	/// the first whitespace token of `psyche_init.command`, file-name only, run through
556	/// [`normalize_basename`](spt_runtime::resolve::normalize_basename) (lowercase /
557	/// `.exe`-strip / dot-stem) so it keys the same as a live exe basename. `None` for
558	/// an empty command. // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
559	fn psyche_program_norm(command: &str) -> Option<String> {
560	    let token = command.split_whitespace().next()?;
561	    let file = Path::new(token).file_name()?.to_string_lossy().into_owned();
562	    Some(spt_runtime::resolve::normalize_basename(&file))
563	}
564
565	/// Whether `pid` is the live `{id}-psyche` orphan we may reap — the ID-SPECIFIC
566	/// recycle guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP). NOT basename-only: on a
567	/// shared box sibling agents share the `claude` basename, so a recycled pid landing
568	/// on a SIBLING'S `claude` would match basename alone → a wrong-kill of a live agent.
569	/// Requires ALL THREE — alive, exe basename == the adapter's psyche program, AND the
570	/// command line contains the FULL psyche id `<id>-psyche` (baked into the psyche argv
571	/// via `{id}`; a sibling `agent-other-psyche` never contains `agent-psyche`). FAILS
572	/// SAFE: any None (dead / unreadable basename / unreadable cmdline) ⇒ `false`, i.e.
573	/// decline to reap — a missed dup is cheap (Breap bounds it at daemon-stop), a
574	/// wrong-kill is catastrophic.
575	///
576	/// CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's
577	/// `psyche_init.command` uses `{id}` (the norm — a Psyche needs its identity;
578	/// spt-claude-code does). An adapter that omits `{id}` SAFELY MISSES this reap
579	/// (the dup lingers until daemon-stop/Breap, exactly today's behavior) — never a
580	/// wrong-kill. If a real non-`{id}` psyche adapter ever ships, add a start-time
581	/// discriminator (process younger than the perch spawn stamp ⇒ spare).
582	// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
583	fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {
584	    if !spt_store::proc::is_process_alive(pid) {
585	        return false;
586	    }
587	    let basename_ok = spt_store::proc::exe_basename(pid)
588	        .map(|b| spt_runtime::resolve::normalize_basename(&b) == expected_prog_norm)
589	        .unwrap_or(false);
590	    if !basename_ok {
591	        return false;
592	    }
593	    spt_store::proc::process_cmdline(pid)
594	        .map(|cl| cl.contains(psyche_id))
595	        .unwrap_or(false) // cmdline unreadable ⇒ fail safe (decline to reap)
596	}
597
598	/// Brain-start orphan-psyche reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): a bare
599	/// brain restart leaves the PRIOR brain's `{id}-psyche` processes ALIVE — `stop_host`

/// the first whitespace token of `psyche_init.command`, file-name only, run through
/// [`normalize_basename`](spt_runtime::resolve::normalize_basename) (lowercase /
/// `.exe`-strip / dot-stem) so it keys the same as a live exe basename. `None` for
/// an empty command. // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn psyche_program_norm(command: &str) -> Option<String> {
    let token = command.split_whitespace().next()?;
    let file = Path::new(token).file_name()?.to_string_lossy().into_owned();
    Some(spt_runtime::resolve::normalize_basename(&file))
}

/// Whether `pid` is the live `{id}-psyche` orphan we may reap — the ID-SPECIFIC
/// recycle guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP). NOT basename-only: on a
/// shared box sibling agents share the `claude` basename, so a recycled pid landing
/// on a SIBLING'S `claude` would match basename alone → a wrong-kill of a live agent.
/// Requires ALL THREE — alive, exe basename == the adapter's psyche program, AND the
/// command line contains the FULL psyche id `<id>-psyche` (baked into the psyche argv
/// via `{id}`; a sibling `agent-other-psyche` never contains `agent-psyche`). FAILS
/// SAFE: any None (dead / unreadable basename / unreadable cmdline) ⇒ `false`, i.e.
/// decline to reap — a missed dup is cheap (Breap bounds it at daemon-stop), a
/// wrong-kill is catastrophic.
///
/// CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's
/// `psyche_init.command` uses `{id}` (the norm — a Psyche needs its identity;
/// spt-claude-code does). An adapter that omits `{id}` SAFELY MISSES this reap
/// (the dup lingers until daemon-stop/Breap, exactly today's behavior) — never a
/// wrong-kill. If a real non-`{id}` psyche adapter ever ships, add a start-time
/// discriminator (process younger than the perch spawn stamp ⇒ spare).
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {
    if !spt_store::proc::is_process_alive(pid) {
        return false;
    }
    let basename_ok = spt_store::proc::exe_basename(pid)
        .map(|b| spt_runtime::resolve::normalize_basename(&b) == expected_prog_norm)
        .unwrap_or(false);
    if !basename_ok {
        return false;
    }
    spt_store::proc::process_cmdline(pid)
        .map(|cl| cl.contains(psyche_id))
        .unwrap_or(false) // cmdline unreadable ⇒ fail safe (decline to reap)
}

/// Brain-start orphan-psyche reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): a bare
/// brain restart leaves the PRIOR brain's `{id}-psyche` processes ALIVE — `stop_host`


Guard confirmed: gate is `normalize_basename(exe_basename(pid)) == expected_prog_norm`. Deriving prog from the psyche's live exe makes the basename gate tautological for both children — isolating the test to the cmdline (id-specific) gate, exactly as doyle intends. Apply FIX1 + FIX2 to the orphan_reap test.


Edit

        #[cfg(unix)]
        let spawn = |marker: &str| {
            std::process::Command::new("sh")
                .args(["-c", &format!("sleep 30 #{marker}")])
                .spawn()
                .expect("spawn")
        };
        let mut psyche = spawn("agent7-psyche"); // the real {id} psyche
        let mut sibling = spawn("agent7-other-psyche"); // a sibling agent, same basename
        let prog = psyche_program_norm(if cfg!(windows) { "cmd" } else { "sh" }).unwrap();

        // Wait until the real psyche's cmdline is queryable + carries its marker.

        // Two commands with a trailing builtin (`:`) so the shell stays RESIDENT (no
        // tail-exec-replace by `sleep`) and its /proc/<pid>/cmdline retains the full
        // `-c` string incl the marker — portable across sh variants.
        #[cfg(unix)]
        let spawn = |marker: &str| {
            std::process::Command::new("sh")
                .args(["-c", &format!("sleep 30; : {marker}")])
                .spawn()
                .expect("spawn")
        };
        let mut psyche = spawn("agent7-psyche"); // the real {id} psyche
        let mut sibling = spawn("agent7-other-psyche"); // a sibling agent, same basename

        // Wait until the real psyche's cmdline is queryable + carries its marker.


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\livehost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        #[cfg(unix)]
        let spawn = |marker: &str| {
            std::process::Command::new("sh")
                .args(["-c", &format!("sleep 30 #{marker}")])
                .spawn()
                .expect("spawn")
        };
        let mut psyche = spawn("agent7-psyche"); // the real {id} psyche
        let mut sibling = spawn("agent7-other-psyche"); // a sibling agent, same basename
        let prog = psyche_program_norm(if cfg!(windows) { "cmd" } else { "sh" }).unwrap();

        // Wait until the real psyche's cmdline is queryable + carries its marker.

        // Two commands with a trailing builtin (`:`) so the shell stays RESIDENT (no
        // tail-exec-replace by `sleep`) and its /proc/<pid>/cmdline retains the full
        // `-c` string incl the marker — portable across sh variants.
        #[cfg(unix)]
        let spawn = |marker: &str| {
            std::process::Command::new("sh")
                .args(["-c", &format!("sleep 30; : {marker}")])
                .spawn()
                .expect("spawn")
        };
        let mut psyche = spawn("agent7-psyche"); // the real {id} psyche
        let mut sibling = spawn("agent7-other-psyche"); // a sibling agent, same basename

        // Wait until the real psyche's cmdline is queryable + carries its marker.

+        // Two commands with a trailing builtin (`:`) so the shell stays RESIDENT (no

+        // tail-exec-replace by `sleep`) and its /proc/<pid>/cmdline retains the full

+        // `-c` string incl the marker — portable across sh variants.

-                .args(["-c", &format!("sleep 30 #{marker}")])

+                .args(["-c", &format!("sleep 30; : {marker}")])

-        let prog = psyche_program_norm(if cfg!(windows) { "cmd" } else { "sh" }).unwrap();


                std::thread::sleep(Duration::from_millis(20));
            }
        };
        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());

                std::thread::sleep(Duration::from_millis(20));
            }
        };
        // Derive the expected program norm from the psyche's OWN live exe basename
        // (computed AFTER the readiness poll, so the process is queryable) instead of
        // hardcoding "sh"/"cmd": on some Linux distros /bin/sh is a symlink (dash), so a
        // hardcoded "sh" mismatches the resolved /proc/<pid>/exe basename and wrongly
        // fails the basename gate. The test's real subject is the ID-SPECIFIC (cmdline)
        // gate — the sibling shares this exact basename and is spared only by its
        // differing cmdline.
        let prog = spt_runtime::resolve::normalize_basename(
            &spt_store::proc::exe_basename(psyche.id()).expect("psyche exe basename"),
        );
        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());


                std::thread::sleep(Duration::from_millis(20));
            }
        };
        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());

                std::thread::sleep(Duration::from_millis(20));
            }
        };
        // Derive the expected program norm from the psyche's OWN live exe basename
        // (computed AFTER the readiness poll, so the process is queryable) instead of
        // hardcoding "sh"/"cmd": on some Linux distros /bin/sh is a symlink (dash), so a
        // hardcoded "sh" mismatches the resolved /proc/<pid>/exe basename and wrongly
        // fails the basename gate. The test's real subject is the ID-SPECIFIC (cmdline)
        // gate — the sibling shares this exact basename and is spared only by its
        // differing cmdline.
        let prog = spt_runtime::resolve::normalize_basename(
            &spt_store::proc::exe_basename(psyche.id()).expect("psyche exe basename"),
        );
        let real = ready && psyche_orphan_should_reap("agent7-psyche", &prog, psyche.id());

+        // Derive the expected program norm from the psyche's OWN live exe basename

+        // (computed AFTER the readiness poll, so the process is queryable) instead of

+        // hardcoding "sh"/"cmd": on some Linux distros /bin/sh is a symlink (dash), so a

+        // hardcoded "sh" mismatches the resolved /proc/<pid>/exe basename and wrongly

+        // fails the basename gate. The test's real subject is the ID-SPECIFIC (cmdline)

+        // gate — the sibling shares this exact basename and is spared only by its

+        let prog = spt_runtime::resolve::normalize_basename(

+            &spt_store::proc::exe_basename(psyche.id()).expect("psyche exe basename"),

// The sibling: same basename, but its cmdline carries "agent7-other-psyche",

// NOT "agent7-psyche" → the id-specific gate spares it.


Now the proc.rs marker test (FIX2 consistency) + confirm `psyche_program_norm` still used in prod (no dead-code warning).


Bash

cd /c/Users/decid/Documents/projects/spt-core
echo "=== psyche_program_norm callers (non-test) ==="; grep -rn "psyche_program_norm" crates/spt-daemon/src/livehost.rs
echo "=== proc.rs marker test ==="; grep -n "process_cmdline_reads_a_live_arg_marker" crates/spt-store/src/proc.rs

Check psyche_program_norm usage + locate proc.rs marker test


=== psyche_program_norm callers (non-test) ===
559:fn psyche_program_norm(command: &str) -> Option<String> {
633:            .and_then(|role| psyche_program_norm(&role.command))
=== proc.rs marker test ===
452:    fn process_cmdline_reads_a_live_arg_marker() {


`psyche_program_norm` still used at livehost.rs:633 (prod) — no dead-code. Read proc.rs marker test.


445	    // [unit->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP] process_cmdline reads a live
446	    // process's argv — the ID-SPECIFIC half of the orphan-reap guard. A child
447	    // launched with a unique MARKER token in its command line reports a cmdline
448	    // CONTAINING that marker; a dead pid yields None (fail-safe → no reap). This
449	    // also guards the Windows NtQueryInformationProcess(ProcessCommandLineInformation)
450	    // FFI: a broken read would fail this in-test, never ship.
451	    #[test]
452	    fn process_cmdline_reads_a_live_arg_marker() {
453	        let marker = "spt-cmdline-marker-7f3a";
454	        // A long-lived child whose own command line carries the marker.
455	        #[cfg(windows)]
456	        let mut child = {
457	            let mut c = std::process::Command::new("cmd");
458	            c.args(["/C", &format!("ping -n 30 127.0.0.1 >NUL & rem {marker}")]);
459	            c.spawn().expect("spawn marker child")
460	        };
461	        #[cfg(unix)]
462	        let mut child = {
463	            let mut c = std::process::Command::new("sh");
464	            c.args(["-c", &format!("sleep 30 #{marker}")]);
465	            c.spawn().expect("spawn marker child")
466	        };
467	        let pid = child.id();
468	        // The child may take a beat to be queryable; poll briefly.
469	        let mut found = false;
470	        for _ in 0..100 {
471	            if let Some(cl) = process_cmdline(pid) {
472	                if cl.contains(marker) {
473	                    found = true;
474	                    break;
475	                }
476	            }
477	            std::thread::sleep(std::time::Duration::from_millis(20));
478	        }
479	        let _ = child.kill();
480	        let _ = child.wait();
481	        assert!(found, "process_cmdline must read a live child's argv marker");
482	        assert!(
483	            process_cmdline(2_000_000_000).is_none(),
484	            "a dead pid yields no cmdline (fail-safe → caller declines to reap)"
485	        );
486	        assert!(process_cmdline(0).is_none(), "pid 0 is never probe-able");
487	    }
488	}
489

    // [unit->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP] process_cmdline reads a live
    // process's argv — the ID-SPECIFIC half of the orphan-reap guard. A child
    // launched with a unique MARKER token in its command line reports a cmdline
    // CONTAINING that marker; a dead pid yields None (fail-safe → no reap). This
    // also guards the Windows NtQueryInformationProcess(ProcessCommandLineInformation)
    // FFI: a broken read would fail this in-test, never ship.
    #[test]
    fn process_cmdline_reads_a_live_arg_marker() {
        let marker = "spt-cmdline-marker-7f3a";
        // A long-lived child whose own command line carries the marker.
        #[cfg(windows)]
        let mut child = {
            let mut c = std::process::Command::new("cmd");
            c.args(["/C", &format!("ping -n 30 127.0.0.1 >NUL & rem {marker}")]);
            c.spawn().expect("spawn marker child")
        };
        #[cfg(unix)]
        let mut child = {
            let mut c = std::process::Command::new("sh");
            c.args(["-c", &format!("sleep 30 #{marker}")]);
            c.spawn().expect("spawn marker child")
        };
        let pid = child.id();
        // The child may take a beat to be queryable; poll briefly.
        let mut found = false;
        for _ in 0..100 {
            if let Some(cl) = process_cmdline(pid) {
                if cl.contains(marker) {
                    found = true;
                    break;
                }
            }
            std::thread::sleep(std::time::Duration::from_millis(20));
        }
        let _ = child.kill();
        let _ = child.wait();
        assert!(found, "process_cmdline must read a live child's argv marker");
        assert!(
            process_cmdline(2_000_000_000).is_none(),
            "a dead pid yields no cmdline (fail-safe → caller declines to reap)"
        );
        assert!(process_cmdline(0).is_none(), "pid 0 is never probe-able");
    }
}


Apply FIX2 to proc.rs marker test (consistency / bulletproof).


Edit

        #[cfg(unix)]
        let mut child = {
            let mut c = std::process::Command::new("sh");
            c.args(["-c", &format!("sleep 30 #{marker}")]);
            c.spawn().expect("spawn marker child")
        };

        // Two commands ending in the builtin `:` keep the shell RESIDENT (no
        // tail-exec-replace by `sleep`), so its /proc/<pid>/cmdline retains the full
        // `-c` string incl the marker across sh variants (e.g. dash).
        #[cfg(unix)]
        let mut child = {
            let mut c = std::process::Command::new("sh");
            c.args(["-c", &format!("sleep 30; : {marker}")]);
            c.spawn().expect("spawn marker child")
        };


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\proc.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        #[cfg(unix)]
        let mut child = {
            let mut c = std::process::Command::new("sh");
            c.args(["-c", &format!("sleep 30 #{marker}")]);
            c.spawn().expect("spawn marker child")
        };

        // Two commands ending in the builtin `:` keep the shell RESIDENT (no
        // tail-exec-replace by `sleep`), so its /proc/<pid>/cmdline retains the full
        // `-c` string incl the marker across sh variants (e.g. dash).
        #[cfg(unix)]
        let mut child = {
            let mut c = std::process::Command::new("sh");
            c.args(["-c", &format!("sleep 30; : {marker}")]);
            c.spawn().expect("spawn marker child")
        };

+        // Two commands ending in the builtin `:` keep the shell RESIDENT (no

+        // tail-exec-replace by `sleep`), so its /proc/<pid>/cmdline retains the full

+        // `-c` string incl the marker across sh variants (e.g. dash).

-            c.args(["-c", &format!("sleep 30 #{marker}")]);

+            c.args(["-c", &format!("sleep 30; : {marker}")]);


All test-only edits done. Windows seam bar (serial, target-seam): clippy --workspace, the two changed units, traceable.


Bash

cd /c/Users/decid/Documents/projects/spt-core
echo "=== CLIPPY WORKSPACE ==="
CARGO_TARGET_DIR=target-seam cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -8
echo "CLIPPY_EXIT=${PIPESTATUS[0]}"
echo "=== spt-daemon orphan_reap unit ==="
CARGO_TARGET_DIR=target-seam cargo test -p spt-daemon --lib orphan_reap_is_id_specific -- --test-threads=1 2>&1 | grep -E "test result:|^test .* \.\.\. (ok|FAILED)|panicked|error"
echo "DAEMON_EXIT=${PIPESTATUS[0]}"
echo "=== spt-store proc marker unit ==="
CARGO_TARGET_DIR=target-seam cargo test -p spt-store --lib process_cmdline_reads_a_live_arg_marker -- --test-threads=1 2>&1 | grep -E "test result:|^test .* \.\.\. (ok|FAILED)|panicked|error"
echo "STORE_EXIT=${PIPESTATUS[0]}"
echo "=== traceable ==="
traceable-reqs check 2>&1 | tail -2; echo "TRACEABLE_EXIT=${PIPESTATUS[0]}"

Windows seam bar: clippy workspace + 2 changed units + traceable


Command running in background with ID: bqyf0gbqt. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bqyf0gbqt.output. You will be notified when it completes. To check interim output, use Read on that file path.


Seam bar running (clippy compile + units + traceable). Awaiting completion.


1	=== CLIPPY WORKSPACE ===
2

=== CLIPPY WORKSPACE ===


Compiling. Wait for completion.


ack wait


awaiting bqyf0gbqt seam bar


Test-only fix applied (FIX1 derive prog from live exe_basename, FIX2 hardened unix markers in both units, Windows spawns unchanged, zero production change). Windows seam bar running. On green → commit → push PR #25 → ping doyle SEAM-GREEN + watch CI both runners.


<task-notification>
<task-id>bqyf0gbqt</task-id>
<tool-use-id>toolu_01Soq8PWxD3ZswdWndfgrnxV</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bqyf0gbqt.output</output-file>
<status>completed</status>
<summary>Background command "Windows seam bar: clippy workspace + 2 changed units + traceable" completed (exit code 0)</summary>
</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.


Seam bar exit 0. Read results.


1	=== CLIPPY WORKSPACE ===
2	    Checking spt-term v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
3	    Checking spt-net v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
4	    Checking spt-live v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
5	    Checking mock-adapter v0.11.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
6	    Checking spt-daemon v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
7	    Checking spt v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
8	    Checking xtask v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
9	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.47s
10	CLIPPY_EXIT=0
11	=== spt-daemon orphan_reap unit ===
12	test livehost::tests::orphan_reap_is_id_specific_spares_a_same_basename_sibling ... ok
13	test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 324 filtered out; finished in 0.01s
14	DAEMON_EXIT=0
15	=== spt-store proc marker unit ===
16	test proc::tests::process_cmdline_reads_a_live_arg_marker ... ok
17	test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 166 filtered out; finished in 0.01s
18	STORE_EXIT=0
19	=== traceable ===
20	  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
21	       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
22	TRACEABLE_EXIT=0
23

=== CLIPPY WORKSPACE ===
    Checking spt-term v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Checking spt-net v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-live v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    Checking mock-adapter v0.11.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Checking spt-daemon v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Checking xtask v0.11.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.47s
CLIPPY_EXIT=0
=== spt-daemon orphan_reap unit ===
test livehost::tests::orphan_reap_is_id_specific_spares_a_same_basename_sibling ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 324 filtered out; finished in 0.01s
DAEMON_EXIT=0
=== spt-store proc marker unit ===
test proc::tests::process_cmdline_reads_a_live_arg_marker ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 166 filtered out; finished in 0.01s
STORE_EXIT=0
=== traceable ===
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
TRACEABLE_EXIT=0


Windows bar GREEN: clippy EXIT=0 · orphan_reap unit ok · proc marker unit ok · traceable EXIT=0. Linux fix verified only via CI (can't run dash locally). Commit test-only + push PR #25.


cd /c/Users/decid/Documents/projects/spt-core
git add crates/spt-daemon/src/livehost.rs crates/spt-store/src/proc.rs
git diff --cached --stat
git commit -q -F - <<'EOF'
v0.12.0 — fix CI: cross-platform orphan-reap + cmdline-marker test fixtures

Linux CI (kitsubito) RED on the dup-reap unit: on Ubuntu /bin/sh is a dash
symlink, so the spawned `sh -c` child's /proc/<pid>/exe resolves to "dash" and
normalize_basename("dash") != the hardcoded "sh" -> the basename gate failed ->
the "real {id} psyche is reapable" assert fired. Test-fixture portability only;
the production reap guard (psyche_orphan_should_reap / exe_basename /
process_cmdline) is correct and UNCHANGED.

- orphan_reap_is_id_specific_spares_a_same_basename_sibling: derive the expected
  program norm from the psyche's OWN live exe basename (after the readiness poll)
  instead of hardcoding "sh"/"cmd". The test's real subject is the ID-SPECIFIC
  (cmdline) discrimination -- the sibling shares this exact basename and is spared
  only by its differing cmdline.
- both that test and proc::process_cmdline_reads_a_live_arg_marker: harden the
  unix marker spawn to `sh -c 'sleep 30; : <marker>'` (two commands, trailing
  builtin) so the shell stays resident (no tail-exec-replace) and /proc/cmdline
  retains the marker across sh variants. Windows spawns unchanged.

Co-authored by: todlando
EOF
git log --oneline -1
echo "=== push ==="
git push origin v0.12.0-lifecycle 2>&1 | tail -5

Commit test-only CI fix + push PR #25


 crates/spt-daemon/src/livehost.rs | 16 ++++++++++++++--
 crates/spt-store/src/proc.rs      |  5 ++++-
 2 files changed, 18 insertions(+), 3 deletions(-)
6de905b v0.12.0 — fix CI: cross-platform orphan-reap + cmdline-marker test fixtures
=== push ===
To https://github.com/SaberMage/spt-core.git
   464eaf7..6de905b  v0.12.0-lifecycle -> v0.12.0-lifecycle


Pushed @6de905b → PR #25 CI re-triggers. Ping doyle + arm a CI monitor (both runners must go green: Linux fix + Windows now that disk's reclaimed).


cd /c/Users/decid/Documents/projects/spt-core
$OWL send doyle <<'EOF'
PUSHED @6de905b to PR#25 (test-only fix). Windows bar GREEN local: clippy --workspace EXIT0, orphan_reap unit ok, proc marker unit ok, traceable EXIT0. Linux dash-symlink fix verifiable only via CI (can't run dash locally). CI re-triggered — watching both runners (kitsubito Linux + hfenduleam Windows). Will ping you when both GREEN for re-gate. NO production change (psyche_orphan_should_reap/exe_basename/process_cmdline untouched).
EOF

Ping doyle: fix pushed, watching CI


➜ SENT:doyle


Monitor

PR #25 CI both runners settle

sleep 90
while true; do
  s=$(gh pr view 25 --json statusCheckRollup 2>/dev/null || true)
  if [ -n "$s" ]; then
    pending=$(echo "$s" | jq -r '[.statusCheckRollup[] | select(.status!="COMPLETED")] | length' 2>/dev/null || echo 1)
    if [ "$pending" = "0" ]; then
      echo "CI SETTLED:"
      echo "$s" | jq -r '.statusCheckRollup[] | "\(.name): \(.conclusion)"'
      break
    fi
  fi
  sleep 60
done


Monitor started (task buyjj6ewt, timeout 2400000ms). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply.


Fix pushed @6de905b, doyle pinged, CI monitor armed (both runners). On settle: if green → ping doyle for re-gate; if Linux still red → diagnose; Windows flake/infra → re-enqueue.


ScheduleWakeup

CI monitor is the real wake signal for PR#25 both-runner settle; this is fallback heartbeat.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 05:00:00 (in 1855s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Standing by for CI settle on PR #25.


<task-notification>
<task-id>bzoq0kglt</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="ce2f9edc" type="msg" from="doyle">Code-read PASS on @6de905b — fix is exactly right: FIX1 derives prog from the psyche's live exe_basename after the readiness poll; FIX2 hardens both unix spawns to `sleep 30; : &lt;marker&gt;`; Windows unchanged; ZERO production change. Traced the Linux path: sh-&gt;dash so prog=&quot;dash&quot;, real psyche cmdline c</EVENT-PART>
<EVENT-PART seq="2/4" id="ce2f9edc">ontains &quot;agent7-psyche&quot; -&gt; reapable, sibling &quot;agent7-other-psyche&quot; does NOT contain &quot;agent7-psyche&quot; -&gt; spared. Correct.

HEADS UP on CI: the WINDOWS runner (hfenduleam) will almost certainly RE-RED on &quot;no space on device&quot; at link — that's the GLOBAL C: disk (1.85 TB used outside the workspa</EVENT-PART>
<EVENT-PART seq="3/4" id="ce2f9edc">ce), NOT your fix. Operator disk-reclaim is pending. So:
- Watch the LINUX runner (kitsubito) — that's the real verdict on your fix. Green kitsubito = fix confirmed.
- A Windows red right now = disk, ignore it; don't chase it or re-push.
I re-gate fully (and resume deployah) only once BOTH runners are green, which needs the operator</EVENT-PART>
<EVENT-PART seq="4/4" id="ce2f9edc"> to free C: first. I'm surfacing the disk to the operator now. Ping me when kitsubito goes green; I'll hold the final both-green re-gate for the disk reclaim.</EVENT-PART></event>
</task-notification>


doyle: code-read PASS @6de905b. Windows runner will likely RE-RED on global C: disk-full ("no space on device" at link) — NOT my fix; ignore, don't chase/re-push. **Linux kitsubito = the real verdict.** doyle holds final both-green re-gate for the operator disk-reclaim; ping him when kitsubito green. My monitor already settles on all-complete — I'll read kitsubito specifically. ACK.


Bash

$OWL send doyle <<'EOF'
ACK code-read PASS + Windows-disk heads-up. CI monitor armed; I'll read the LINUX kitsubito `test` conclusion specifically — green there = fix confirmed, I ping you. Windows red = global C: disk, I ignore it (no chase, no re-push). You hold both-green re-gate for the operator disk-reclaim. Standing by.
EOF

ACK doyle CI guidance


ACK sent. Awaiting CI-settle event; will read kitsubito Linux `test` conclusion. Green → ping doyle (fix confirmed); Windows red = disk, ignore.


# Autonomous loop tick (dynamic pacing)

Run the autonomous check using the loop instructions established earlier in this conversation. If you cannot find them, treat this as a no-op tick.

You scheduled this tick via the ScheduleWakeup tool (not a recurring cron). To keep the loop alive, call ScheduleWakeup again at the end of this turn with `prompt` set to the literal sentinel `<<autonomous-loop-dynamic>>` — otherwise the loop ends after this tick.

If a Monitor is armed (check TaskList), keep `delaySeconds` at 1200–1800s — the Monitor is the wake signal and this is only the fallback heartbeat. If you were woken by a `<task-notification>`, handle the event before rescheduling. To stop the loop, also TaskStop the monitor (use TaskList to find its task ID if no longer in context).


Claude resuming /loop wakeup (Jun 18 4:59am)


Fallback heartbeat. CI monitor still armed (no settle event yet). Quick direct check of kitsubito state to advance if already settled.


cd /c/Users/decid/Documents/projects/spt-core
gh pr view 25 --json statusCheckRollup 2>/dev/null | jq -r '.statusCheckRollup[] | "\(.name): \(.status)/\(.conclusion)"'

Quick PR #25 check state


Error: Exit code 127
/usr/bin/bash: line 8: jq: command not found


jq missing in this bash — my Monitor command uses jq → broken (never settles). Use `gh`'s built-in `--jq`. Check state + I'll re-arm a jq-free monitor.


cd /c/Users/decid/Documents/projects/spt-core
echo "=== gh built-in --jq ==="
gh pr view 25 --json statusCheckRollup --jq '.statusCheckRollup[] | "\(.name): \(.status)/\(.conclusion)"' 2>&1
echo "=== gh pr checks (tabular) ==="
gh pr checks 25 2>&1 | head -20

PR #25 check state via gh built-in jq


=== gh built-in --jq ===
test (self-hosted, Linux, kitsubito): COMPLETED/SUCCESS
test (self-hosted, Windows, hfenduleam): COMPLETED/SUCCESS
n1-gate (self-hosted, Linux, kitsubito): COMPLETED/SUCCESS
n1-gate (self-hosted, Windows, hfenduleam): COMPLETED/SUCCESS
traceability: COMPLETED/SUCCESS
twohost-b: COMPLETED/SKIPPED
twohost-a: COMPLETED/SKIPPED
=== gh pr checks (tabular) ===
twohost-a	skipping	0	https://github.com/SaberMage/spt-core/actions/runs/27756305663/job/82120408278
twohost-b	skipping	0	https://github.com/SaberMage/spt-core/actions/runs/27756305663/job/82120408081
n1-gate (self-hosted, Linux, kitsubito)	pass	1m14s	https://github.com/SaberMage/spt-core/actions/runs/27756305663/job/82118915812
n1-gate (self-hosted, Windows, hfenduleam)	pass	1m50s	https://github.com/SaberMage/spt-core/actions/runs/27756305663/job/82118915838
test (self-hosted, Linux, kitsubito)	pass	5m53s	https://github.com/SaberMage/spt-core/actions/runs/27756305663/job/82118915879
test (self-hosted, Windows, hfenduleam)	pass	6m54s	https://github.com/SaberMage/spt-core/actions/runs/27756305663/job/82118915890
traceability	pass	8s	https://github.com/SaberMage/spt-core/actions/runs/27756305663/job/82118915940


**BOTH runners GREEN** — run 27756305663 (post @6de905b): test Linux kitsubito ✓ + test Windows hfenduleam ✓ (disk freed) + n1-gate both ✓ + traceability ✓. Full both-green doyle wanted. Stop the broken (jq-less) monitor + ping doyle.


Successfully stopped task: buyjj6ewt (sleep 90
while true; do
  s=$(gh pr view 25 --json statusCheckRollup 2>/dev/null || true)
  if [ -n "$s" ]; then
    pending=$(echo "$s" | jq -r '[.statusCheckRollup[] | select(.status!="COMPLETED")] | length' 2>/dev/null || echo 1)
    if [ "$pending" = "0" ]; then
      echo "CI SETTLED:"
      echo "$s" | jq -r '.statusCheckRollup[] | "\(.name): \(.conclusion)"'
      break
    fi
  fi
  sleep 60
done)


$OWL send doyle <<'EOF'
PR#25 CI GREEN BOTH RUNNERS (run 27756305663, post @6de905b): test Linux kitsubito PASS (your fix CONFIRMED — dash-symlink resolved) + test Windows hfenduleam PASS (C: disk freed, link succeeded) + n1-gate both PASS + traceability PASS + twohost skipped. No reds. Mergeable. Ready for your final both-green re-gate → deployah resumes v0.12.0 release (counter 25). Standing by.
EOF

Ping doyle: both runners green


Both green, doyle pinged, broken monitor stopped. Record the CI fix + the jq lesson, then reschedule.


14	**v0.12.0 STATE @464eaf7 (NOT pushed) — ALL ITEMS GATE-PASS, READY TO RELEASE:** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap + endpoint-purge @981dea2 + **ready-agent-resume @464eaf7 = doyle GATE-PASS 2026-06-18** (independently: read diff, confirmed append-dedup L99-100, ran new unit + int + positive anchor live_bind_firsthost in target-seam all green, traceable EXIT=0, FIX(2) SUBSUMED state-gate verified-at-code; int non-vacuous — diag shows carried session sess-resume-carry-42, state=ready_agent, hosted=0). todlando told CLEAR. **NEXT: drive deployah → v0.12.0 MINOR release, AWAITING operator GO** (publish is outward-facing/irreversible, all prior releases operator-gated; stack 11 ahead of main, unpushed). REQ-PICKER-ADAPTER-DESCRIPTION = deferred fast-follow (post-v0.12.0).
15	- **REQ-READY-AGENT-RESUME** @464eaf7 [doc,impl,unit,int]: offline ReadyAgent now shows in `endpoint run` picker Resume-from-history. ROOT: harness-hosted ready bind ReadyAgent::start_homed wrote info.json but never ledgered → 0 session rows → picker (gates on ledger rows) skipped it. **FIX(1)** start_homed appends Boot row on bind (mirror establish_perch:250, !sid.is_empty, best-effort). **FIX(2)=SUBSUMED (verified-at-code B4-style, ZERO code):** no-psyche discriminator for a ready endpoint is the livehost reconcile START-side STATE gate (livehost.rs:319 `info.state != LIVE_AGENT_STATE → continue`, skips a ready_agent perch) AHEAD of the adapter(325)/psyche_init(343) gates; cmd_endpoint_run type-agnostic; --resume carries session via {session_id}/[env] fill. unit start_ledgers_a_boot_session_row. int ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind w/ OWL_SESSION_ID ledgers carried session + reconcile_once hosts NO psyche EVEN with a live-capable psyche_init adapter resolved = inversion of live_bind_firsthost). NO `spt` subcommand → no xtask gen needed. **GOTCHA: targeted `cargo test -p spt --test contract_e2e` shows 2 FAIL (`mock_bin.exists()`) — contract_e2e needs the `mock-session` helper which lives in the `mock-adapter` PACKAGE, not spt; targeted --test won't build it. Pre-build `cargo build -p mock-adapter --bin mock-session`. NOT a regression.** Seam bar all green (clippy workspace, traceable EXIT=0, new unit+int, poll_envelope/contract/quickstart/live_bind_firsthost siblings).

**v0.12.0 STATE @464eaf7 (NOT pushed) — ALL ITEMS GATE-PASS, READY TO RELEASE:** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap + endpoint-purge @981dea2 + **ready-agent-resume @464eaf7 = doyle GATE-PASS 2026-06-18** (independently: read diff, confirmed append-dedup L99-100, ran new unit + int + positive anchor live_bind_firsthost in target-seam all green, traceable EXIT=0, FIX(2) SUBSUMED state-gate verified-at-code; int non-vacuous — diag shows carried session sess-resume-carry-42, state=ready_agent, hosted=0). todlando told CLEAR. **NEXT: drive deployah → v0.12.0 MINOR release, AWAITING operator GO** (publish is outward-facing/irreversible, all prior releases operator-gated; stack 11 ahead of main, unpushed). REQ-PICKER-ADAPTER-DESCRIPTION = deferred fast-follow (post-v0.12.0).
- **REQ-READY-AGENT-RESUME** @464eaf7 [doc,impl,unit,int]: offline ReadyAgent now shows in `endpoint run` picker Resume-from-history. ROOT: harness-hosted ready bind ReadyAgent::start_homed wrote info.json but never ledgered → 0 session rows → picker (gates on ledger rows) skipped it. **FIX(1)** start_homed appends Boot row on bind (mirror establish_perch:250, !sid.is_empty, best-effort). **FIX(2)=SUBSUMED (verified-at-code B4-style, ZERO code):** no-psyche discriminator for a ready endpoint is the livehost reconcile START-side STATE gate (livehost.rs:319 `info.state != LIVE_AGENT_STATE → continue`, skips a ready_agent perch) AHEAD of the adapter(325)/psyche_init(343) gates; cmd_endpoint_run type-agnostic; --resume carries session via {session_id}/[env] fill. unit start_ledgers_a_boot_session_row. int ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind w/ OWL_SESSION_ID ledgers carried session + reconcile_once hosts NO psyche EVEN with a live-capable psyche_init adapter resolved = inversion of live_bind_firsthost). NO `spt` subcommand → no xtask gen needed. **GOTCHA: targeted `cargo test -p spt --test contract_e2e` shows 2 FAIL (`mock_bin.exists()`) — contract_e2e needs the `mock-session` helper which lives in the `mock-adapter` PACKAGE, not spt; targeted --test won't build it. Pre-build `cargo build -p mock-adapter --bin mock-session`. NOT a regression.** Seam bar all green (clippy workspace, traceable EXIT=0, new unit+int, poll_envelope/contract/quickstart/live_bind_firsthost siblings).


Edit

**v0.12.0 STATE @464eaf7 (NOT pushed) — ALL ITEMS GATE-PASS, READY TO RELEASE:**

**RELEASE IN FLIGHT: PR #25 (v0.12.0-lifecycle → main) CI GREEN BOTH RUNNERS @6de905b (run 27756305663) — awaiting doyle final re-gate → deployah resumes v0.12.0 (counter 25).** deployah pushed the stack + opened PR#25; first CI RED on Linux kitsubito = TEST-ONLY cross-platform bug (NOT my production code): dup-reap unit hardcoded prog="sh" but Ubuntu /bin/sh→dash symlink → exe_basename="dash" → basename gate failed. FIX @6de905b (doyle-dispatched, code-read PASS, ZERO prod change): (1) orphan_reap test derives expected prog from the psyche's OWN live exe_basename AFTER readiness poll (not hardcoded sh/cmd) — isolates the test to the ID-SPECIFIC cmdline gate; (2) harden unix marker spawn `sh -c 'sleep 30; : <marker>'` (two cmds, trailing builtin `:` → shell stays resident, no tail-exec-replace, /proc/cmdline keeps marker) in BOTH orphan_reap + proc.rs marker units; Windows spawns unchanged. Earlier Windows red = global C: disk-full (1.85TB outside workspace), cleared by operator reclaim → both green this run. **LESSON: this Git Bash has NO `jq` — a CI Monitor using external `jq` silently never settles; use `gh pr view --json X --jq '...'` (gh's BUILT-IN jq) or `gh pr checks`.**

**v0.12.0 STATE @464eaf7 (now @6de905b on PR#25) — ALL ITEMS GATE-PASS, READY TO RELEASE:**


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

**RELEASE IN FLIGHT: PR #25 (v0.12.0-lifecycle → main) CI GREEN BOTH RUNNERS @6de905b (run 27756305663) — awaiting doyle final re-gate → deployah resumes v0.12.0 (counter 25).** deployah pushed the stack + opened PR#25; first CI RED on Linux kitsubito = TEST-ONLY cross-platform bug (NOT my production code): dup-reap unit hardcoded prog="sh" but Ubuntu /bin/sh→dash symlink → exe_basename="dash" → basename gate failed. FIX @6de905b (doyle-dispatched, code-read PASS, ZERO prod change): (1) orphan_reap test derives expected prog from the psyche's OWN live exe_basename AFTER readiness poll (not hardcoded sh/cmd) — isolates the test to the ID-SPECIFIC cmdline gate; (2) harden unix marker spawn `sh -c 'sleep 30; : <marker>'` (two cmds, trailing builtin `:` → shell stays resident, no tail-exec-replace, /proc/cmdline keeps marker) in BOTH orphan_reap + proc.rs marker units; Windows spawns unchanged. Earlier Windows red = global C: disk-full (1.85TB outside workspace), cleared by operator reclaim → both green this run. **LESSON: this Git Bash has NO `jq` — a CI Monitor using external `jq` silently never settles; use `gh pr view --json X --jq '...'` (gh's BUILT-IN jq) or `gh pr checks`.**

**v0.12.0 STATE @464eaf7 (now @6de905b on PR#25) — ALL ITEMS GATE-PASS, READY TO RELEASE:**

**UNIFYING ROOT:** info.json `status=online` is a ONE-WAY LATCH — set at establish (startup.rs:361/468), NEVER cleared vs real liveness (liveness.rs:80-93 is_perch_alive returns ONLINE for daemon-hosted). Every symptom falls out of this + stop/restart lifecycle not tearing-down/rehydrating.

-**v0.12.0 STATE @464eaf7 (NOT pushed) — ALL ITEMS GATE-PASS, READY TO RELEASE:** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap + endpoint-purge @981dea2 + **ready-agent-resume @464eaf7 = doyle GATE-PASS 2026-06-18** (independently: read diff, confirmed append-dedup L99-100, ran new unit + int + positive anchor live_bind_firsthost in target-seam all green, traceable EXIT=0, FIX(2) SUBSUMED state-gate verified-at-code; int non-vacuous — diag shows carried session sess-resume-carry-42, state=ready_agent, hosted=0). todlando told CLEAR. **NEXT: drive deployah → v0.12.0 MINOR release, AWAITING operator GO** (publish is outward-facing/irreversible, all prior releases operator-gated; stack 11 ahead of main, unpushed). REQ-PICKER-ADAPTER-DESCRIPTION = deferred fast-follow (post-v0.12.0).

+**RELEASE IN FLIGHT: PR #25 (v0.12.0-lifecycle → main) CI GREEN BOTH RUNNERS @6de905b (run 27756305663) — awaiting doyle final re-gate → deployah resumes v0.12.0 (counter 25).** deployah pushed the stack + opened PR#25; first CI RED on Linux kitsubito = TEST-ONLY cross-platform bug (NOT my production code): dup-reap unit hardcoded prog="sh" but Ubuntu /bin/sh→dash symlink → exe_basename="dash" → basename gate failed. FIX @6de905b (doyle-dispatched, code-read PASS, ZERO prod change): (1) orphan_reap test derives expected prog from the psyche's OWN live exe_basename AFTER readiness poll (not hardcoded sh/cmd) — isolates the test to the ID-SPECIFIC cmdline gate; (2) harden unix marker spawn `sh -c 'sleep 30; : <marker>'` (two cmds, trailing builtin `:` → shell stays resident, no tail-exec-replace, /proc/cmdline keeps marker) in BOTH orphan_reap + proc.rs marker units; Windows spawns unchanged. Earlier Windows red = global C: disk-full (1.85TB outside workspace), cleared by operator reclaim → both green this run. **LESSON: this Git Bash has NO `jq` — a CI Monitor using external `jq` silently never settles; use `gh pr view --json X --jq '...'` (gh's BUILT-IN jq) or `gh pr checks`.**

+**v0.12.0 STATE @464eaf7 (now @6de905b on PR#25) — ALL ITEMS GATE-PASS, READY TO RELEASE:** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap + endpoint-purge @981dea2 + **ready-agent-resume @464eaf7 = doyle GATE-PASS 2026-06-18** (independently: read diff, confirmed append-dedup L99-100, ran new unit + int + positive anchor live_bind_firsthost in target-seam all green, traceable EXIT=0, FIX(2) SUBSUMED state-gate verified-at-code; int non-vacuous — diag shows carried session sess-resume-carry-42, state=ready_agent, hosted=0). todlando told CLEAR. **NEXT: drive deployah → v0.12.0 MINOR release, AWAITING operator GO** (publish is outward-facing/irreversible, all prior releases operator-gated; stack 11 ahead of main, unpushed). REQ-PICKER-ADAPTER-DESCRIPTION = deferred fast-follow (post-v0.12.0).

- **REQ-READY-AGENT-RESUME** @464eaf7 [doc,impl,unit,int]: offline ReadyAgent now shows in `endpoint run` picker Resume-from-history. ROOT: harness-hosted ready bind ReadyAgent::start_homed wrote info.json but never ledgered → 0 session rows → picker (gates on ledger rows) skipped it. **FIX(1)** start_homed appends Boot row on bind (mirror establish_perch:250, !sid.is_empty, best-effort). **FIX(2)=SUBSUMED (verified-at-code B4-style, ZERO code):** no-psyche discriminator for a ready endpoint is the livehost reconcile START-side STATE gate (livehost.rs:319 `info.state != LIVE_AGENT_STATE → continue`, skips a ready_agent perch) AHEAD of the adapter(325)/psyche_init(343) gates; cmd_endpoint_run type-agnostic; --resume carries session via {session_id}/[env] fill. unit start_ledgers_a_boot_session_row. int ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind w/ OWL_SESSION_ID ledgers carried session + reconcile_once hosts NO psyche EVEN with a live-capable psyche_init adapter resolved = inversion of live_bind_firsthost). NO `spt` subcommand → no xtask gen needed. **GOTCHA: targeted `cargo test -p spt --test contract_e2e` shows 2 FAIL (`mock_bin.exists()`) — contract_e2e needs the `mock-session` helper which lives in the `mock-adapter` PACKAGE, not spt; targeted --test won't build it. Pre-build `cargo build -p mock-adapter --bin mock-session`. NOT a regression.** Seam bar all green (clippy workspace, traceable EXIT=0, new unit+int, poll_envelope/contract/quickstart/live_bind_firsthost siblings).

- **REQ-ENDPOINT-PURGE** @981dea2 [doc(doyle@d8e7761),impl,unit,int]: `spt endpoint purge <id>` — EndpointCmd::Purge{id,--yes,--force} + cmd_endpoint_purge. Offline-only gate (--force=cmd_stop→bounded-wait-reconcile-unhost+psyche-reap→purge), SPT_ENDPOINT_ID self-guard, confirm unless --yes. Removes perch TREE (remove_dir_all owlery/<id>/ = nested {id}-psyche/{id}-w*/shells, all under it) + unregister_address + ContextStore::remove_endpoint + AccessStore::open + NEW VisibilityStore::remove_endpoint. Generalizes fork --delete-source behind rename's offline gate. **GOTCHA: xtask gen BUILDS spt respecting CARGO_TARGET_DIR but READS hardcoded target/debug/spt → under CARGO_TARGET_DIR=target-seam the gen reads a STALE binary, reference.md misses the new command. FIX: run `cargo run -p xtask -- gen` in DEFAULT target (where CI's xtask check reads).** unit purge_offline_gate_and_self_guard + int purge_removes_every_record (cli.rs bin tests — spt has NO lib, use `--bin spt`). Seam: clippy clean, traceable 247 EXIT=0, xtask check OK, 178 bin tests green.


Memory updated (CI fix + jq lesson). PR #25 both-green, doyle pinged for final re-gate → deployah resumes release. No CI monitor needed now; owl listener wakes me on doyle's reply. Reschedule fallback heartbeat.


ScheduleWakeup

PR#25 both-green, awaiting doyle re-gate via owl listener (real wake); fallback heartbeat.

Prompt: <<autonomous-loop-dynamic>>


<owl_messages>
<owl_message from="doyle" priority="high">RE-GATE PASS — independently verified run 27756305663 success @6de905b (test green both runners, n1-gate both, traceability green). Your Linux dash-symlink fix is CONFIRMED on kitsubito. You are CLEAR — v0.12.0 fully gated end-to-end again. Driving deployah to resume the release now. Thanks for the fast turnaround.</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.


Next wakeup scheduled for 05:31:00 (in 1839s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


doyle **RE-GATE PASS** @6de905b — both-runner green confirmed, Linux dash fix verified on kitsubito. CLEAR. doyle driving deployah to resume v0.12.0 release. ACK.


Bash

$OWL send doyle <<'EOF'
ACK re-gate PASS. v0.12.0 fully gated end-to-end, release in deployah's hands (counter 25). todlando CLEAR + idle. Ping if the release surfaces anything. 🦉
EOF

ACK doyle re-gate pass


➜ SENT:doyle


v0.12.0 closed out. CI fix landed (@6de905b), both runners green, doyle re-gate PASS. Release now deployah's (counter 25). todlando CLEAR + idle — owl listener live for any release fallout.


Claude resuming /loop wakeup (Jun 18 5:31am)


1	---
2	name: v012-lifecycle-milestone
3	description: "v0.12.0 spt-hosted lifecycle & liveness reconciliation milestone (operator-mandated blocking, doyle dispatch) — 7 REQs, wave-by-wave, B2 keystone built"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: bf7260cd-fc4e-463b-9c46-b2ed704b4bab
8	---
9
10	**v0.12.0 spt-hosted LIFECYCLE & LIVENESS RECONCILIATION** — operator-mandated BLOCKING (endpoint run unusable: attach hangs, daemon stop doesn't stop). doyle dispatched 2026-06-17 (2-agent diagnosis). Branch **v0.12.0-lifecycle** off main@27e0619 (v0.11.0). todlando builds keystone-first in waves, doyle gates PER-WAVE, then deployah → v0.12.0 MINOR.
11
12	**UNIFYING ROOT:** info.json `status=online` is a ONE-WAY LATCH — set at establish (startup.rs:361/468), NEVER cleared vs real liveness (liveness.rs:80-93 is_perch_alive returns ONLINE for daemon-hosted). Every symptom falls out of this + stop/restart lifecycle not tearing-down/rehydrating.
13
14	**v0.12.0 PUBLISHED 2026-06-18 (counter 25, MINOR, no retag).** Hashes (signed rel-primary-2026): linux `0458b5ae94a16207a61fddcfb10d1f087c95ea129c253d2af57454b8ca4df785` · win `6699be3d4256ccc64ab34ecb51bae6fe56aa4e4fcc80019db969f6176de3b6cd`. https://github.com/SaberMage/spt-releases/releases/tag/v0.12.0 . Path: PR#25 merge c19f6a1 (incl reap-fix 6de905b) → bump c17550d (CHANGELOG=doyle VET PASS body verbatim + Cargo.lock 11 first-party only) → main CI Windows green + Linux rerun green (flaked once on `broker::spawn_env_reaches_child` = PTY output-vs-exit race, out="" = no-output-captured NOT env-drop, NOT perri's F-013 class; non-blocking follow-up = drain OUTPUT after KIND_EXIT in that test) → tag → sign+publish. **doyle informed perri (delivered): lifecycle unblocks her /sptc:live (brain-restart no-wedge/no-dup, attach no-hang, daemon-stop reaps, no phantom revival, status-reachability with RELAY EXEMPT so her relay agents aren't wrongly offlined); F-013 NO_PERCH was v0.11.0 not this; her FINDING A pointer-adapter gap still queued separately.** deployah publisher leg 19× clean (counter 7-25); 7 releases this revive session v0.8.3→v0.12.0. **MILESTONE COMPLETE.** ORIGINAL IN-FLIGHT NOTES below. deployah pushed the stack + opened PR#25; first CI RED on Linux kitsubito = TEST-ONLY cross-platform bug (NOT my production code): dup-reap unit hardcoded prog="sh" but Ubuntu /bin/sh→dash symlink → exe_basename="dash" → basename gate failed. FIX @6de905b (doyle-dispatched, code-read PASS, ZERO prod change): (1) orphan_reap test derives expected prog from the psyche's OWN live exe_basename AFTER readiness poll (not hardcoded sh/cmd) — isolates the test to the ID-SPECIFIC cmdline gate; (2) harden unix marker spawn `sh -c 'sleep 30; : <marker>'` (two cmds, trailing builtin `:` → shell stays resident, no tail-exec-replace, /proc/cmdline keeps marker) in BOTH orphan_reap + proc.rs marker units; Windows spawns unchanged. Earlier Windows red = global C: disk-full (1.85TB outside workspace), cleared by operator reclaim → both green this run. **LESSON: this Git Bash has NO `jq` — a CI Monitor using external `jq` silently never settles; use `gh pr view --json X --jq '...'` (gh's BUILT-IN jq) or `gh pr checks`.**
15
16	**v0.12.0 STATE @464eaf7 (now @6de905b on PR#25) — ALL ITEMS GATE-PASS, READY TO RELEASE:** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap + endpoint-purge @981dea2 + **ready-agent-resume @464eaf7 = doyle GATE-PASS 2026-06-18** (independently: read diff, confirmed append-dedup L99-100, ran new unit + int + positive anchor live_bind_firsthost in target-seam all green, traceable EXIT=0, FIX(2) SUBSUMED state-gate verified-at-code; int non-vacuous — diag shows carried session sess-resume-carry-42, state=ready_agent, hosted=0). todlando told CLEAR. **NEXT: drive deployah → v0.12.0 MINOR release, AWAITING operator GO** (publish is outward-facing/irreversible, all prior releases operator-gated; stack 11 ahead of main, unpushed). REQ-PICKER-ADAPTER-DESCRIPTION = deferred fast-follow (post-v0.12.0).
17	- **REQ-READY-AGENT-RESUME** @464eaf7 [doc,impl,unit,int]: offline ReadyAgent now shows in `endpoint run` picker Resume-from-history. ROOT: harness-hosted ready bind ReadyAgent::start_homed wrote info.json but never ledgered → 0 session rows → picker (gates on ledger rows) skipped it. **FIX(1)** start_homed appends Boot row on bind (mirror establish_perch:250, !sid.is_empty, best-effort). **FIX(2)=SUBSUMED (verified-at-code B4-style, ZERO code):** no-psyche discriminator for a ready endpoint is the livehost reconcile START-side STATE gate (livehost.rs:319 `info.state != LIVE_AGENT_STATE → continue`, skips a ready_agent perch) AHEAD of the adapter(325)/psyche_init(343) gates; cmd_endpoint_run type-agnostic; --resume carries session via {session_id}/[env] fill. unit start_ledgers_a_boot_session_row. int ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind w/ OWL_SESSION_ID ledgers carried session + reconcile_once hosts NO psyche EVEN with a live-capable psyche_init adapter resolved = inversion of live_bind_firsthost). NO `spt` subcommand → no xtask gen needed. **GOTCHA: targeted `cargo test -p spt --test contract_e2e` shows 2 FAIL (`mock_bin.exists()`) — contract_e2e needs the `mock-session` helper which lives in the `mock-adapter` PACKAGE, not spt; targeted --test won't build it. Pre-build `cargo build -p mock-adapter --bin mock-session`. NOT a regression.** Seam bar all green (clippy workspace, traceable EXIT=0, new unit+int, poll_envelope/contract/quickstart/live_bind_firsthost siblings).
18	- **REQ-ENDPOINT-PURGE** @981dea2 [doc(doyle@d8e7761),impl,unit,int]: `spt endpoint purge <id>` — EndpointCmd::Purge{id,--yes,--force} + cmd_endpoint_purge. Offline-only gate (--force=cmd_stop→bounded-wait-reconcile-unhost+psyche-reap→purge), SPT_ENDPOINT_ID self-guard, confirm unless --yes. Removes perch TREE (remove_dir_all owlery/<id>/ = nested {id}-psyche/{id}-w*/shells, all under it) + unregister_address + ContextStore::remove_endpoint + AccessStore::open + NEW VisibilityStore::remove_endpoint. Generalizes fork --delete-source behind rename's offline gate. **GOTCHA: xtask gen BUILDS spt respecting CARGO_TARGET_DIR but READS hardcoded target/debug/spt → under CARGO_TARGET_DIR=target-seam the gen reads a STALE binary, reference.md misses the new command. FIX: run `cargo run -p xtask -- gen` in DEFAULT target (where CI's xtask check reads).** unit purge_offline_gate_and_self_guard + int purge_removes_every_record (cli.rs bin tests — spt has NO lib, use `--bin spt`). Seam: clippy clean, traceable 247 EXIT=0, xtask check OK, 178 bin tests green.
19
20	**WAVE 5: psyche-reap @7449f5e GATE-PASS. dup-reap @909ad01 GATE-PASS + B4 CLOSED SUBSUMED (same commit). SCOPE GREW: operator added 2 more to v0.12.0 — READY-AGENT-RESUME + `spt endpoint purge` — doyle DESIGNS+MINTS+DISPATCHES those (NOT mine to start). So v0.12.0 = mandated-7 (B4 subsumed) + dup-reap + [ready-resume + purge pending doyle design]. After all gate → deployah → MINOR.**

... [29 lines truncated] ...


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


v0.12.0 published (counter 25, PR #25 merged @c19f6a1, tagged + signed) — milestone complete, nothing in flight.


ScheduleWakeup

v0.12.0 shipped + idle; owl listener is the real wake for any doyle/perri ping, fallback heartbeat.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 06:02:00 (in 1837s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Idle — v0.12.0 shipped, nothing actionable.


Claude resuming /loop wakeup (Jun 18 6:02am)


Quiet (3rd idle tick) — v0.12.0 shipped, no messages, nothing in flight.


Idle post-ship; owl listener is real wake for any ping, fallback heartbeat.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 06:33:00 (in 1848s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Idle.


Claude resuming /loop wakeup (Jun 18 6:33am)


Idle — nothing in flight.


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 07:04:00 (in 1851s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


# Autonomous loop tick (dynamic pacing)

Run the autonomous check using the loop instructions established earlier in this conversation. If you cannot find them, treat this as a no-op tick.

You scheduled this tick via the ScheduleWakeup tool (not a recurring cron). To keep the loop alive, call ScheduleWakeup again at the end of this turn with `prompt` set to the literal sentinel `<<autonomous-loop-dynamic>>` — otherwise the loop ends after this tick.

If a Monitor is armed (check TaskList), keep `delaySeconds` at 1200–1800s — the Monitor is the wake signal and this is only the fallback heartbeat. If you were woken by a `<task-notification>`, handle the event before rescheduling. To stop the loop, also TaskStop the monitor (use TaskList to find its task ID if no longer in context).


Claude resuming /loop wakeup (Jun 18 7:04am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 07:35:00 (in 1839s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 7:35am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 08:06:00 (in 1852s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 8:06am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 08:37:00 (in 1841s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 8:37am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 09:08:00 (in 1853s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 9:08am)


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


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 09:39:00 (in 1855s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Idle.


Claude resuming /loop wakeup (Jun 18 9:39am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 10:10:00 (in 1855s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 10:10am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 10:41:00 (in 1854s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


# Autonomous loop tick (dynamic pacing)

Run the autonomous check using the loop instructions established earlier in this conversation. If you cannot find them, treat this as a no-op tick.

You scheduled this tick via the ScheduleWakeup tool (not a recurring cron). To keep the loop alive, call ScheduleWakeup again at the end of this turn with `prompt` set to the literal sentinel `<<autonomous-loop-dynamic>>` — otherwise the loop ends after this tick.

If a Monitor is armed (check TaskList), keep `delaySeconds` at 1200–1800s — the Monitor is the wake signal and this is only the fallback heartbeat. If you were woken by a `<task-notification>`, handle the event before rescheduling. To stop the loop, also TaskStop the monitor (use TaskList to find its task ID if no longer in context).


Claude resuming /loop wakeup (Jun 18 10:41am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 11:12:00 (in 1856s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 11:12am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 11:43:00 (in 1856s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 11:43am)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 12:14:00 (in 1848s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 12:14pm)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 12:45:00 (in 1851s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 12:45pm)


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


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 13:16:00 (in 1840s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Idle.


Claude resuming /loop wakeup (Jun 18 1:16pm)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 13:47:00 (in 1852s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 1:47pm)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 14:18:00 (in 1849s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


# Autonomous loop tick (dynamic pacing)

Run the autonomous check using the loop instructions established earlier in this conversation. If you cannot find them, treat this as a no-op tick.

You scheduled this tick via the ScheduleWakeup tool (not a recurring cron). To keep the loop alive, call ScheduleWakeup again at the end of this turn with `prompt` set to the literal sentinel `<<autonomous-loop-dynamic>>` — otherwise the loop ends after this tick.

If a Monitor is armed (check TaskList), keep `delaySeconds` at 1200–1800s — the Monitor is the wake signal and this is only the fallback heartbeat. If you were woken by a `<task-notification>`, handle the event before rescheduling. To stop the loop, also TaskStop the monitor (use TaskList to find its task ID if no longer in context).


Claude resuming /loop wakeup (Jun 18 2:18pm)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 14:49:00 (in 1853s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 2:49pm)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 15:20:00 (in 1853s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 3:20pm)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 15:51:00 (in 1854s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 3:51pm)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 16:22:00 (in 1855s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Claude resuming /loop wakeup (Jun 18 4:22pm)


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


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 16:53:00 (in 1855s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Idle.


Claude resuming /loop wakeup (Jun 18 4:53pm)


Steady-state idle post-ship; owl listener wakes on any ping.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 17:24:00 (in 1854s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


<task-notification>
<task-id>bzoq0kglt</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="89e43b50" type="msg" from="doyle">NEW SPRINT — v0.12.1 lifecycle reopen. You execute, I gate per-wave, deployah releases.

WHY: v0.12.0 shipped but real-harness testing (operator, claude-spt, Windows) showed the lifecycle fixes DON'T work in the real daemon+broker+PTY path. We gated on mocks + in-proc reconcile_once — green tests, broken reali</EVENT-PART>
<EVENT-PART seq="2/8" id="89e43b50">ty. This sprint is real-harness-gated.

START HERE: branch v0.12.1-lifecycle (pushed, off main@5f9ea23). Read V0.12.1-LIFECYCLE-JIT.md on that branch — it is the authoritative plan (every item, root cause, fix, and gate is spelled out). REQs already minted registry-first in traceable-reqs.toml; activate each REQ's stages when you start</EVENT-PART>
<EVENT-PART seq="3/8" id="89e43b50"> its wave + tag evidence in the same commit.

WAVE ORDER:
1. Wave 1 FIRST — the dummy-harness fixture (operator's idea): a kind=harness adapter whose command is a trivial program that binds its perch on startup, prints stdout on an interval, stays alive until killed. Drives the REAL `spt endpoint run`-&gt;launch_harness_brokered-&gt</EVENT-PART>
<EVENT-PART seq="4/8" id="89e43b50">;broker PTY-&gt;rc attach path. This UNBLOCKS every lifecycle gate. Build it before the fixes.
2. Wave 2 — REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY: CREATE_BREAKAWAY_FROM_JOB on both daemon spawn paths daemon.rs:707 + deelevate.rs:519, AND pin each broker-spawned harness into a daemon-owned Job Object backstop; pty.rs UNCHANGED, ConPTY iso</EVENT-PART>
<EVENT-PART seq="5/8" id="89e43b50">lation is already correct) + REQ-HAZARD-ATTACH-WEDGE (loopback sends fail-fast on a dead/undrained subscriber so one dead stream can't hold a net-runtime worker) + the status=online-offline-within-a-tick check + daemon-stop-ends-everything.
3. Wave 3 — picker P1/P2: INVESTIGATE the root FIRST (report to me before fixing — these may be r</EVENT-PART>
<EVENT-PART seq="6/8" id="89e43b50">eal bugs or symptoms of the broker wedge / stale state).
4. Wave 4 — REQ-ENDPOINT-LIST-MERGE-LOCAL (drop --local, bare list always merges local perches, fix whoami alias; xtask gen in DEFAULT target).

BINDING GATING DISCIPLINE: every lifecycle gate runs against the REAL dummy-harness fixture + a REAL detached daemon — NOT a mock,</EVENT-PART>
<EVENT-PART seq="7/8" id="89e43b50"> NOT in-proc reconcile_once with a live-draining peer. That gap is exactly why v0.12.0 escaped. I re-run each gate independently.

NOTE (operator-confirmed): we are running on LEGACY spt on this machine, so spt-core can be freely modified + its daemon restarted here without touching live-agent coordination — real-harness validation can</EVENT-PART>
<EVENT-PART seq="8/8" id="89e43b50"> use the operator's actual claude-spt setup, not just a scratch dir.

Ping me wave-by-wave SEAM-GREEN. Start with Wave 1 + the Wave 2 keystone. Full per-item detail is in V0.12.1-LIFECYCLE-JIT.md.</EVENT-PART></event>
</task-notification>


New sprint: v0.12.1 lifecycle reopen — v0.12.0 fixes broke in REAL daemon+broker+PTY path (we gated on mocks/in-proc reconcile). Real-harness-gated now. doyle dispatched, JIT plan authoritative on branch. ACK + checkout + read plan.


Bash

cd /c/Users/decid/Documents/projects/spt-core
$OWL send doyle <<'EOF'
ACK v0.12.1 sprint. Understood: v0.12.0 escaped because we gated on mocks + in-proc reconcile_once, not the real daemon+broker+PTY path. New discipline = every lifecycle gate vs REAL dummy-harness fixture + REAL detached daemon (legacy spt here = freely restartable). Checking out v0.12.1-lifecycle, reading V0.12.1-LIFECYCLE-JIT.md + minted REQs now. Starting Wave 1 (dummy-harness fixture) then Wave 2 keystone (VIEWER-CLOSE-DETACH breakaway+Job backstop). Ping you wave-by-wave SEAM-GREEN.
EOF
git fetch origin v0.12.1-lifecycle 2>&1 | tail -2
git checkout v0.12.1-lifecycle 2>&1 | tail -3
git log --oneline -1

ACK doyle + checkout v0.12.1-lifecycle


➜ SENT:doyle
From https://github.com/SaberMage/spt-core
 * branch            v0.12.1-lifecycle -> FETCH_HEAD
Your branch is up to date with 'origin/v0.12.1-lifecycle'.
Already on 'v0.12.1-lifecycle'
a4a76c6 v0.12.1 lifecycle reopen: JIT plan + "view is independent" invariant + mint 5 REQs (registry-first)


On v0.12.1-lifecycle @a4a76c6. Read the JIT plan.


1	# v0.12.1 — Lifecycle + Picker JIT Plan
2
3	**Why:** v0.12.0 shipped (counter 25) but real-harness testing (operator, `claude-spt`, Windows) showed the
4	lifecycle fixes do **not** deliver in the real daemon + broker + PTY path. v0.12.0 was gated on **mock adapters +
5	in-process `reconcile_once`** — green tests, broken reality. v0.12.1 reopens with **real-harness gating** (no mocks).
6
7	**Branch:** `v0.12.1-lifecycle` off `main@5f9ea23`. **Release:** v0.12.1 PATCH (deployah, after doyle gate-pass all waves).
8	**Roles:** doyle designs/mints/gates · todlando executes · deployah releases.
9
10	**Meta-fix (binding for this sprint):** every lifecycle gate runs against the **real dummy-harness fixture + a real
11	detached daemon** — NOT `reconcile_once` in-proc, NOT a mock with a live-draining peer. doyle independently re-runs each.
12
13	---
14
15	## Wave 1 — Test infrastructure (unblocks all real-harness gates)
16
17	- **T1 — dummy-harness adapter fixture** (operator's idea). A `kind="harness"` adapter whose command is a trivial program
18	  that (a) binds its perch on startup (the harness contract), (b) prints a stdout line on an interval, (c) stays alive
19	  until killed. Drives the REAL `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc` attach path.
20	  Isolates "broker wedged / endpoint died" from "a real harness failed to launch." Becomes the permanent regression
21	  fixture the v0.12.0 mock tests never had. Lives in the test tree; runs against a scratch target dir (livehost-E2E pattern).
22
23	## Wave 2 — Lifecycle core
24
25	- **L1 — REQ-HAZARD-VIEWER-CLOSE-DETACH (PRIMARY).** Closing the tab/window where `spt endpoint run` was invoked must
26	  detach only the `spt rc` pump; the daemon-hosted harness keeps running and is re-attachable.
27	  ROOT: the daemon never breaks away from the launching terminal's Windows Job Object (`KILL_ON_JOB_CLOSE`); no
28	  `CREATE_BREAKAWAY_FROM_JOB` anywhere → tab close reaps the daemon's freshly-spawned ConPTY harness subtree. ConPTY
29	  isolation itself is already correct (portable-pty makes the pseudoconsole in the daemon; no console signal/handle leak).
30	  FIX: add `CREATE_BREAKAWAY_FROM_JOB` to both daemon spawn paths (`daemon.rs:707` `detached_no_inherit`,
31	  `deelevate.rs:519` elevated) **+** pin each broker-spawned harness into a **daemon-owned Job Object** (mirror Breap,
32	  `reap.rs`) as backstop (survives even if a terminal sets `SILENT_BREAKAWAY_OK=false`). `pty.rs` unchanged. Unix: verify
33	  the daemon's session-detach already covers terminal-close (SIGHUP scope) — likely no change, add a guard test.
34	  GATE: spawn daemon under a parent-held `KILL_ON_JOB_CLOSE` job → `endpoint run` a dummy harness → close the parent job →
35	  assert the harness pid **stays alive** AND `spt rc <id>` re-attaches AND a brand-new endpoint launches.
36
37	- **L2 — REQ-HAZARD-ATTACH-WEDGE (ROBUSTNESS).** Even a *legitimately* dead PTY child (real crash/kill) + an undrained
38	  operator pump must NOT wedge the broker.
39	  ROOT: loopback attach output is a blocking `write_all` into a bounded 64 KB tokio duplex (`nethost.rs:1040,1090`); a
40	  dead operator (closed tab) stops draining → `write_all` blocks forever (the "loopback never hangs" assumption at
41	  `nethost.rs:1103` is false) → parks workers in the **2-worker** net runtime (`nethost.rs:640`) → both saturate → every
42	  new attach/`endpoint run` stalls after `PUMP_IPC_READER: spawned` → 30 s `FIRST_EVENT_GRACE` → "dead or wedged";
43	  `daemon stop` can't join the stuck workers. Distinct from the removed B1 path-(c) mutex deadlock.
44	  FIX: make loopback sends fail-fast — a full-buffer / `BrokenPipe` loopback write is an ordinary per-stream error that
45	  ENDS `serve_attach`; one dead stream can never hold a runtime worker. (Defense-in-depth: raise worker count — but the
46	  real fix is non-blocking-on-dead-peer.)
47	  GATE (dummy harness): kill the child abruptly + drop the operator pump without a clean detach → assert a **new**
48	  endpoint is still served, `brain.sessions()` returns promptly, `daemon stop` completes bounded.
49
50	- **L3 — status=online persistence (sub-item, folded into L2's gate).** Three dead endpoints stayed `status=online`.
51	  B2 (`reconcile_hosted_liveness`) should offline a controllable spt-hosted perch whose broker session is gone — confirm
52	  whether abrupt Windows child death actually reaps the broker session (so B2 sees it absent) and whether the reconcile
53	  tick fires. Resolve as part of L2; GATE asserts the dead endpoint is marked **offline within one reconcile tick**.
54
55	- **L4 — `daemon stop` ends everything (operator item #1, folded).** `daemon stop` did not end all spt processes / take
56	  everything offline. Largely downstream of L1+L2 (un-wedged broker can stop cleanly; harness in a daemon-owned job is
57	  reaped by Breap). GATE: after L1+L2, `daemon stop` terminates the daemon + brain + all hosted harness/psyche processes,
58	  bounded, and the roster goes offline.
59
60	## Wave 3 — Picker (operator-raised; ROOTS TBD — investigate before fixing)
61
62	- **P1 — REQ-PICKER-HISTORY-FRESH.** `spt endpoint run` picker does **not** show project history for fresh endpoints.
63	  Investigate the project-history loader (v0.10.0 PICKER-2, `picker/data.rs`) — real bug vs "fresh = no history yet"
64	  semantics. Then fix.
65	- **P2 — REQ-PICKER-ONLINE-ACTION.** Picker shows **"Start now"** for endpoints that are already online. Investigate the
66	  4-state status mapping (v0.10.0 PICKER-1, `picker/model.rs`) — is it reading live/online state correctly, or rendering
67	  stale/wedged broker state? Then fix (online → "Attach", not "Start now").
68
69	## Wave 4 — endpoint list cleanup (operator decision 2026-06-17, dropped — now minted)
70
71	- **E1 — REQ-ENDPOINT-LIST-MERGE-LOCAL.** Remove the `--local` flag; the bare `spt endpoint list` ALWAYS merges this
72	  node's local (unadvertised) perches into the view. Rationale: `spt whoami` is a thin alias — a just-online agent
73	  running `whoami` must see its own perch. Drop the flag + its `--detail` conflict test + the v0.10.0 hint line +
74	  `cmd_list_local`; fix the `whoami` alias path. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target).
75
76	## Release
77
78	- **v0.12.1 PATCH.** deployah drives after doyle gate-pass on all waves. CHANGELOG: cross-check **each bullet's scope vs
79	  the commit range** (the v0.12.0 lesson — [[changelog-scope-vs-commit-range]]); name the specific delta. Post-publish:
80	  inform operator + perri (real-harness lifecycle now actually works).
81
82	## Open / carried
83
84	- Non-blocking test-hardening: drain OUTPUT after `KIND_EXIT` in `broker::spawn_env_reaches_child` (latent flake) — fold
85	  into Wave 1/2 test work if cheap, else carry.
86	- Machine cleanup: clear the operator's 3 phantom endpoints (wall-a/b/c) + orphan psyche — scoped, preserve daemon +
87	  the `doyle` live perch + CI.
88

# 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

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


Plan clear. Wave 1 = dummy-harness fixture first (unblocks all real-harness gates). Read the minted v0.12.1 REQs + the existing mock harness (to extend vs build new).


cd /c/Users/decid/Documents/projects/spt-core
echo "=== v0.12.1 minted REQs ==="
grep -n "REQ-HAZARD-VIEWER-CLOSE-DETACH\|REQ-HAZARD-ATTACH-WEDGE\|REQ-PICKER-HISTORY-FRESH\|REQ-PICKER-ONLINE-ACTION\|REQ-ENDPOINT-LIST-MERGE-LOCAL" traceable-reqs.toml
echo "=== mock harness fixture files ==="
ls adapters/mock/ adapters/mock/src/ 2>/dev/null
echo "=== mock-session bin ==="
find . -path ./target -prune -o -name "*.rs" -path "*mock*" -print 2>/dev/null | grep -v target-seam

Locate v0.12.1 REQs + mock harness fixture


=== v0.12.1 minted REQs ===
1139:id = "REQ-HAZARD-VIEWER-CLOSE-DETACH"
1143:id = "REQ-HAZARD-ATTACH-WEDGE"
1147:id = "REQ-PICKER-HISTORY-FRESH"
1151:id = "REQ-PICKER-ONLINE-ACTION"
1153:required_stages = []  # registry-first; investigate-then-activate (may partly resolve once REQ-HAZARD-ATTACH-WEDGE / status reconcile land). Likely [impl,unit] once root is pinned
1155:id = "REQ-ENDPOINT-LIST-MERGE-LOCAL"
=== mock harness fixture files ===
adapters/mock/:
Cargo.toml
manifest.toml
src
tests

adapters/mock/src/:
main.rs
shell.rs
=== mock-session bin ===
./adapters/mock/src/main.rs
./adapters/mock/src/shell.rs
./adapters/mock/tests/manifest_valid.rs