commit 464eaf771f0b784ce2caee4a8f22c47d59678ef2
Author: Reavo End <decidel@gmail.com>
Date:   Thu Jun 18 04:04:45 2026 -0700

    v0.12.0 — REQ-READY-AGENT-RESUME: ledger the ready bind so offline ReadyAgents resume

    FIX(1): ReadyAgent::start_homed appends a Boot session row on bind (mirroring
    establish_perch:250) so an OFFLINE ready perch carries the resume rows the
    picker's offline+LOCAL Resume-from-history gates on — closing the gap that today
    only LiveAgents enjoyed (a ready bind wrote info.json but never the ledger).

    FIX(2) = SUBSUMED, verified-at-code (B4-style), zero code: cmd_endpoint_run is
    type-agnostic and the no-Psyche discriminator for a ready endpoint is the
    livehost reconcile START-side STATE gate (a ready_agent-state perch is skipped
    before the adapter/psyche_init gates), so `endpoint run` of a no-psyche_init
    manifest already yields a ready endpoint with NO Psyche; --resume carries its
    session into the bind for either type. No new bringup mode, no picker changes.

    doc: CONTEXT.md — `spt endpoint run` is the spt-hosted bringup for BOTH endpoint
    types (live=psyche_init present, ready=absent), ReadyAgent first-class in
    Resume-from-history. unit: start_ledgers_a_boot_session_row. int:
    ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind ledgers
    the carried session + reconcile_once hosts NO Psyche even with a live-capable
    psyche_init adapter resolved — proving the state gate). Activated [doc,impl,unit,int].

    Co-authored by: todlando

 CONTEXT.md                                  |  18 +++
 crates/spt-msg/src/ready.rs                 |  47 ++++++-
 crates/spt/tests/ready_resume_ledger_e2e.rs | 186 ++++++++++++++++++++++++++++
 traceable-reqs.toml                         |   2 +-
 4 files changed, 251 insertions(+), 2 deletions(-)
=====FULL DIFF=====
commit 464eaf771f0b784ce2caee4a8f22c47d59678ef2
Author: Reavo End <decidel@gmail.com>
Date:   Thu Jun 18 04:04:45 2026 -0700

    v0.12.0 — REQ-READY-AGENT-RESUME: ledger the ready bind so offline ReadyAgents resume

    FIX(1): ReadyAgent::start_homed appends a Boot session row on bind (mirroring
    establish_perch:250) so an OFFLINE ready perch carries the resume rows the
    picker's offline+LOCAL Resume-from-history gates on — closing the gap that today
    only LiveAgents enjoyed (a ready bind wrote info.json but never the ledger).

    FIX(2) = SUBSUMED, verified-at-code (B4-style), zero code: cmd_endpoint_run is
    type-agnostic and the no-Psyche discriminator for a ready endpoint is the
    livehost reconcile START-side STATE gate (a ready_agent-state perch is skipped
    before the adapter/psyche_init gates), so `endpoint run` of a no-psyche_init
    manifest already yields a ready endpoint with NO Psyche; --resume carries its
    session into the bind for either type. No new bringup mode, no picker changes.

    doc: CONTEXT.md — `spt endpoint run` is the spt-hosted bringup for BOTH endpoint
    types (live=psyche_init present, ready=absent), ReadyAgent first-class in
    Resume-from-history. unit: start_ledgers_a_boot_session_row. int:
    ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind ledgers
    the carried session + reconcile_once hosts NO Psyche even with a live-capable
    psyche_init adapter resolved — proving the state gate). Activated [doc,impl,unit,int].

    Co-authored by: todlando

diff --git a/CONTEXT.md b/CONTEXT.md
index e2827e3..1e6fbe5 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -365,6 +365,24 @@ action enum is the source of truth so a future tap-mode (phone PTY) layers on wi
 re-coupling to keybinds.
 _Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.

+**`spt endpoint run` is the spt-hosted bringup for BOTH endpoint types** (v0.12.0):
+<!-- [doc->REQ-READY-AGENT-RESUME] -->
+The bringup core is **type-agnostic** — the endpoint TYPE is the adapter manifest's
+concern, not a separate bringup mode. A manifest declaring `[session.psyche_init]`
+brings up a **LiveAgent** (the daemon reconcile hosts its Psyche); a manifest *without*
+it brings up a **ReadyAgent** (a poll listener, no Psyche — see *ReadyAgent* and the
+harness-hosted ready bind at the *seed + bind-time resolution* note above). No
+`--adapter`/picker branch distinguishes them: the daemon live-host reconcile hosts only a
+perch whose **state is `live_agent`** (a `ready_agent`-state perch is skipped at the
+reconcile start-side state gate), and within the live path keys on `psyche_init` presence
+— so `endpoint run` of a ready manifest naturally yields a ready endpoint with no Psyche;
+`--resume <session>` carries its session into the bind for either type. Consequently a
+ReadyAgent is now first-class in the **Resume-from-history** offer above: the
+harness-hosted ready bind ledgers a **Boot session row** on bind — exactly as the live
+`establish_perch` path does — so an *offline* ready perch carries the session rows the
+offline+LOCAL Resume-from-history enumerates (previously only LiveAgents did; a ready bind
+wrote `info.json` but never the ledger, so the picker never offered it).
+
 **`spt-<id>` shortcut** (picker `s` keybind, M12-W2):
 <!-- [doc->REQ-RUN-SHORTCUT] -->
 From any pre-start options set, `s` writes (or updates) a **`<basename>-<id>` launcher** at the
diff --git a/crates/spt-msg/src/ready.rs b/crates/spt-msg/src/ready.rs
index 742fc05..40563c6 100644
--- a/crates/spt-msg/src/ready.rs
+++ b/crates/spt-msg/src/ready.rs
@@ -70,11 +70,12 @@ impl ReadyAgent {

         // info.json first, so the record exists by the time the address is live.
         let prior = info::read_info(&perch_path);
+        let sid = session_id();
         let mut rec = InfoJson::new(
             id,
             &now_stamp(),
             std::process::id(),
-            &session_id(),
+            &sid,
             "ready_agent",
         );
         let subnets = spt_store::subnet::SubnetStore::load();
@@ -98,6 +99,26 @@ impl ReadyAgent {
         fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
             .map_err(|e| format!("Failed to write ready marker: {}", e))?;

+        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind
+        // writes info.json directly and — unlike the live `establish_perch` path
+        // (startup.rs:250) — never ledgered, so an offline ready perch carried
+        // ZERO session rows and the picker's offline Resume-from-history (which
+        // gates on ledger rows) never offered it. Append a Boot row here, mirroring
+        // the live path: the boot row dedups against a same-session re-bind, an
+        // empty session is not ledgered, and a ledger problem must never fail the
+        // bind (best-effort).
+        // [impl->REQ-READY-AGENT-RESUME]
+        if !sid.is_empty() {
+            let _ = spt_store::sessions::append(
+                &perch_path,
+                &spt_store::sessions::SessionEntry {
+                    ts: spt_store::timefmt::now_rfc3339(),
+                    session_id: sid.clone(),
+                    trigger: spt_store::sessions::SessionTrigger::Boot,
+                },
+            );
+        }
+
         // Bind + register the listener address. Blocking with a short poll so
         // recv() stays responsive.
         let listener = PollListener::bind_blocking(id, RECV_POLL)?;
@@ -293,4 +314,28 @@ mod tests {
         assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
         assert!(info::read_info(&perch_path).is_some());
     }
+
+    // [unit->REQ-READY-AGENT-RESUME] a ready bind ledgers a Boot session row, so
+    // an OFFLINE ready perch carries resume rows and the picker's
+    // Resume-from-history offers it (closing the live-only gap). The session row
+    // SURVIVES soft_cleanup (only the ready marker is removed) — the offline state
+    // the picker reads.
+    #[test]
+    fn start_ledgers_a_boot_session_row() {
+        crate::test_home::isolate();
+        let id = unique_id("test-ready-ledger");
+        let (mut agent, _) = ReadyAgent::start(&id).unwrap();
+        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
+
+        // The bind wrote exactly one Boot row keyed on the agent's session id.
+        let rows = spt_store::sessions::read_all(&perch_path);
+        assert_eq!(rows.len(), 1, "ready bind must write one ledger row");
+        assert_eq!(rows[0].trigger, spt_store::sessions::SessionTrigger::Boot);
+        assert!(!rows[0].session_id.is_empty());
+
+        // Goes offline → the resume rows the picker gates on persist.
+        agent.soft_cleanup();
+        let offline_rows = spt_store::sessions::read_all(&perch_path);
+        assert_eq!(offline_rows.len(), 1, "ledger survives soft_cleanup (offline)");
+    }
 }
diff --git a/crates/spt/tests/ready_resume_ledger_e2e.rs b/crates/spt/tests/ready_resume_ledger_e2e.rs
new file mode 100644
index 0000000..4a94eac
--- /dev/null
+++ b/crates/spt/tests/ready_resume_ledger_e2e.rs
@@ -0,0 +1,186 @@
+//! REQ-READY-AGENT-RESUME int — a harness-hosted READY endpoint (the `spt ready`
+//! bind, `ReadyAgent::start_homed`) now LEDGERS a Boot session row, so an OFFLINE
+//! ready perch carries resume rows and the picker's offline Resume-from-history
+//! offers it (closing the gap that today only live agents enjoyed) — AND the daemon
+//! reconcile never gives such an endpoint a Psyche, even when its adapter manifest
+//! is fully live-capable.
+//!
+//! The no-Psyche discriminator for a ready endpoint is the reconcile START-side
+//! STATE gate (`livehost.rs`: `info.state != LIVE_AGENT_STATE` → skip), which fires
+//! BEFORE the adapter/`psyche_init` gates: the endpoint TYPE is the manifest/bind's
+//! concern, exactly as the operator scoped it (NO new bringup mode, NO picker
+//! changes — `endpoint run` stays type-agnostic). This test is the direct inversion
+//! of `live_bind_firsthost_e2e`: the SAME `psyche_init`-declaring (live-capable)
+//! adapter that hosts a Psyche for a `live_agent`-state perch hosts NOTHING here,
+//! purely because the bound perch's state is `ready_agent` — isolating the state
+//! gate from the manifest.
+//!
+//! The ready bind is the REAL `spt ready <id>` listener (`run_listen` →
+//! `start_homed`), spawned with `OWL_SESSION_ID` set — the same session that
+//! `endpoint run --resume <session>` carries into the bind (`--resume` →
+//! `{session_id}` fill → `[env] OWL_SESSION_ID` → `start_homed` adopts it; the
+//! fill mechanism itself is unit-covered in `harnesshost`). The ledgered Boot row
+//! is asserted to carry that exact session, so a resumed ready endpoint comes back
+//! on its own session. Scoped pid reap only (shared-runner safe) — never a
+//! machine-wide kill.
+// [int->REQ-READY-AGENT-RESUME]
+
+use std::path::{Path, PathBuf};
+use std::process::{Child, Command, Stdio};
+use std::sync::Arc;
+use std::time::{Duration, Instant};
+
+mod common;
+use common::CommandNoWindowExt;
+
+use spt_daemon::brainproc::StartReason;
+use spt_daemon::config::DaemonConfig;
+use spt_daemon::livehost::{self, LiveSet};
+use spt_store::perch::{self, ParentHint};
+
+#[cfg(windows)]
+const NOOP: &str = "cmd /C rem";
+#[cfg(unix)]
+const NOOP: &str = "true";
+
+/// Scoped pid-tree kill (mirrors the sibling livehost E2Es) — never machine-wide.
+fn kill_pid(pid: u32) {
+    #[cfg(windows)]
+    let _ = Command::new("taskkill")
+        .no_window()
+        .args(["/PID", &pid.to_string(), "/F", "/T"])
+        .output();
+    #[cfg(unix)]
+    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
+}
+
+/// Reap any REAL daemon that auto-started against this throwaway home (hygiene).
+fn reap_daemon(home: &Path) {
+    if let Ok(p) = std::fs::read_to_string(home.join("daemon.pid")) {
+        if let Ok(p) = p.trim().parse::<u32>() {
+            kill_pid(p);
+        }
+    }
+}
+
+#[test]
+fn ready_bind_ledgers_and_reconcile_hosts_no_psyche() {
+    // ── (1) Isolated SPT_HOME. ──
+    let home = tempfile::tempdir().unwrap();
+    std::env::set_var("SPT_HOME", home.path());
+
+    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
+
+    // ── (2) A fully LIVE-CAPABLE adapter (declares `[session.psyche_init]`) — the
+    //    same shape `live_bind_firsthost_e2e` uses to PROVE a live_agent gets hosted.
+    //    Registered so the reconcile resolves the manifest from disk by adapter. The
+    //    point: even THIS adapter hosts no Psyche for a ready_agent-state perch. ──
+    let drops = home.path().join("drops");
+    std::fs::create_dir_all(&drops).unwrap();
+    let dir = drops.to_string_lossy().replace('\\', "/");
+    let manifest_toml = format!(
+        "[adapter]\nname=\"mockready\"\nkind=\"harness\"\nversion=\"1\"\n\
+         min_spt_core_version=\"0\"\n\n\
+         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
+         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
+    );
+    let src = perch::spt_home().join("srcs").join("mockready");
+    std::fs::create_dir_all(&src).unwrap();
+    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
+    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
+
+    // ── (3) The REAL ready bind: `spt ready <id>` (run_listen → start_homed) with
+    //    OWL_SESSION_ID set — the session `endpoint run --resume` carries into the
+    //    bind. It blocks (persistent listener), so spawn it and poll the on-disk
+    //    ledger. ──
+    let id = "ready-resume-int";
+    let session = "sess-resume-carry-42";
+    let mut listener: Child = Command::new(&spt_bin)
+        .no_window()
+        .args(["ready", id])
+        .env("SPT_HOME", home.path())
+        .env("OWL_SESSION_ID", session)
+        .stdout(Stdio::null())
+        .stderr(Stdio::null())
+        .spawn()
+        .expect("spawn `spt ready <id>` (the harness-hosted ready bind)");
+
+    let perch = perch::resolve_perch_path(id, ParentHint::Infer);
+    let deadline = Instant::now() + Duration::from_secs(25);
+    let mut rows = Vec::new();
+    while Instant::now() < deadline {
+        rows = spt_store::sessions::read_all(&perch);
+        if !rows.is_empty() {
+            break;
+        }
+        std::thread::sleep(Duration::from_millis(100));
+    }
+
+    // ── (4) FIX (1): the ready bind ledgered a Boot row keyed on the carried
+    //    session. Capture diagnostics before any assert so a failure reaps cleanly. ──
+    let info_before = spt_store::info::read_info(&perch);
+    let ledger_ok = rows.len() == 1
+        && rows.first().map(|r| r.trigger) == Some(spt_store::sessions::SessionTrigger::Boot)
+        && rows.first().map(|r| r.session_id.as_str()) == Some(session);
+
+    // ── (5) Represent the endpoint-run adapter binding: stamp the resolved adapter
+    //    onto the (real, ready_agent-state) perch. State stays `ready_agent` — the
+    //    reconcile START-side gate keys on STATE, so this only makes the perch a
+    //    realistic, manifest-resolvable endpoint (proving the skip is the state gate,
+    //    NOT a missing/unregistered adapter). ──
+    if let Some(mut rec) = info_before.clone() {
+        rec.adapter = Some("mockready".to_string());
+        let _ = spt_store::info::write_info(&perch, &rec);
+        let _ = spt_store::info::set_status(&perch, spt_store::liveness::STATUS_ONLINE);
+    }
+
+    // ── (6) FIX (2) SUBSUMED: the daemon reconcile hosts NO Psyche for the
+    //    ready_agent-state endpoint, even with the live-capable adapter resolved. ──
+    let set = Arc::new(LiveSet::new());
+    let cfg = DaemonConfig {
+        pulse_period: Duration::from_millis(15),
+        ..DaemonConfig::default()
+    };
+    let registered = spt_runtime::registry::registered(&perch::adapters_dir());
+    livehost::reconcile_once(
+        &perch::owlery_dir(),
+        &registered,
+        &perch::adapters_dir(),
+        &set,
+        &cfg,
+        StartReason::Cold,
+    );
+    let hosted = set.len();
+    let psyche_perch =
+        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
+    let psyche_appeared = spt_store::info::read_info(&psyche_perch).is_some();
+
+    eprintln!(
+        "=== DIAGNOSTIC: ledger_rows={:?} state={:?} hosted={hosted} psyche_appeared={psyche_appeared} ===",
+        rows,
+        info_before.as_ref().map(|i| i.state.clone()),
+    );
+
+    // ── (7) Reap scoped, BEFORE asserting (so a failure never leaks the listener). ──
+    kill_pid(listener.id());
+    let _ = listener.kill();
+    let _ = listener.wait();
+    reap_daemon(home.path());
+
+    assert!(
+        ledger_ok,
+        "REQ-READY-AGENT-RESUME FIX(1): a ready bind must ledger exactly one Boot row \
+         carrying the bound session ({session}); got {rows:?}"
+    );
+    assert_eq!(
+        hosted, 0,
+        "REQ-READY-AGENT-RESUME FIX(2): a ready_agent-state endpoint must never be \
+         Psyche-hosted, even with a live-capable (psyche_init) adapter resolved — the \
+         reconcile START-side state gate skips it"
+    );
+    assert!(
+        !psyche_appeared,
+        "REQ-READY-AGENT-RESUME FIX(2): no `{id}-psyche` perch may appear for a ready \
+         endpoint (the type is the manifest/bind's concern, not a host decision)"
+    );
+}
diff --git a/traceable-reqs.toml b/traceable-reqs.toml
index 87c4f76..666d429 100644
--- a/traceable-reqs.toml
+++ b/traceable-reqs.toml
@@ -612,7 +612,7 @@ required_stages = ["doc", "impl", "unit", "int"]  # doc by doyle (CONTEXT.md `sp
 [[requirements]]
 id = "REQ-READY-AGENT-RESUME"
 title = "An offline ReadyAgent shows in `spt endpoint run`'s picker Resume-from-history and resumes correctly — closing the gap that today only LiveAgents do. ROOT: a harness-hosted ready bind (ReadyAgent::start_homed, ready.rs) writes info.json DIRECTLY and never appends the session ledger (unlike the shared establish_perch:250 live path), so a ready agent — though it has a session_id — produces ZERO ledger rows → the picker's offline+local Resume-from-history (which gates on ledger rows) never offers it. FIX (1): ledger the ready bind (ReadyAgent::start_homed → sessions::append Boot, mirroring establish_perch). FIX (2): `spt endpoint run --resume <session>` honors the adapter MANIFEST's endpoint TYPE — a ReadyAgent manifest (no [session.psyche_init]) resumes as a ready endpoint (poll listener, NO psyche-host); a LiveAgent (with psyche_init) as live. NO new bringup mode + NO picker changes (operator 2026-06-18): `spt endpoint run` is the spt-hosted ENDPOINT bringup for BOTH types, the type IS the adapter-manifest's concern (psyche-host already keys on psyche_init presence) — so (2) likely already holds; VERIFY at code, build only the residual. (v0.12.0)"
-required_stages = []  # registry-first (operator add v0.12.0); activate [impl,unit,int] when built. impl = ReadyAgent::start_homed ledgers the bind (sessions::append) + any endpoint-type-honoring residual in cmd_endpoint_run; unit = a ready bind writes a ledger row (resume_rows non-empty for an offline ready perch); int = bring up a no-psyche_init (ready) endpoint via endpoint run → it ledgers + hosts NO psyche + resumes via --resume
+required_stages = ["doc", "impl", "unit", "int"]  # operator add v0.12.0; built. FIX(2)=SUBSUMED (verified-at-code, ZERO code): cmd_endpoint_run is type-agnostic, the no-psyche discriminator is livehost reconcile's start-side STATE gate (info.state != live_agent → skip a ready_agent perch) ahead of the psyche_init gate. doc = CONTEXT.md `spt endpoint run` is the bringup for BOTH types. impl = ReadyAgent::start_homed ledgers a Boot row (sessions::append, mirror establish_perch:250). unit = start_ledgers_a_boot_session_row (ready bind writes one Boot row, survives soft_cleanup/offline). int = ready_bind_ledgers_and_reconcile_hosts_no_psyche (real `spt ready` bind ledgers the carried session + reconcile_once hosts NO psyche even with a live-capable psyche_init adapter resolved — state gate)
 [[requirements]]
 id = "REQ-PICKER-ADAPTER-DESCRIPTION"
 title = "The Create-new adapter-CHOICE screen of `spt endpoint run`'s picker shows a right-hand Description panel (like the Pick-existing endpoint picker's two-pane) surfacing per-adapter detail: install date, last-updated, adapter TYPE / the endpoint types it hosts, and the adapter description — so the user can see WHAT each adapter is before choosing it (today the selector lists bare names). DEFERRED fast-follow to v0.12.0 (operator 2026-06-18). (post-v0.12.0)"


Diff clean, well-targeted. Two code-level checks before running:
1. **Dedup claim** — impl comment says "boot row dedups against same-session re-bind" but `append` called unconditionally. Verify `sessions::append` actually dedups (else re-bind dupes Boot rows).
2. **Int non-vacuousness** — it's the *inversion* of `live_bind_firsthost`; relies on the sibling proving the positive (live→hosted). Must confirm sibling uses same manifest shape + asserts hosting, else `hosted==0` here proves nothing.


97:pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {
98-    let mut entries = read_all(perch);
99-    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {
100-        return Ok(false); // same session re-bound — not a new boundary
101-    }
102-    entries.push(entry.clone());
103-    let start = entries.len().saturating_sub(MAX_LEDGER);
104-    let body: String = entries[start..]
105-        .iter()
106-        .map(|e| serde_json::to_string(e).unwrap_or_default())
107-        .map(|l| l + "\n")
108-        .collect();
109-    std::fs::create_dir_all(perch)?;
110-    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;
111-    Ok(true)
112-}
113-
114-#[cfg(test)]
115-mod tests {
116-    use super::*;
117-
118-    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {
119-        SessionEntry {
120-            ts: crate::timefmt::rfc3339_utc(1_700_000_000),
121-            session_id: sid.to_string(),
122-            trigger,
123-        }
124-    }
125-
126-    // [unit->REQ-TERM-6] rows append in order and read back oldest→newest.
127-    #[test]
128-    fn appends_and_reads_in_order() {
129-        let dir = tempfile::tempdir().unwrap();
130-        let perch = dir.path().join("perch");
131-        assert!(append(&perch, &entry("s1", SessionTrigger::Boot)).unwrap());
132-        assert!(append(&perch, &entry("s2", SessionTrigger::Clear)).unwrap());
133-        let all = read_all(&perch);
134-        assert_eq!(all.len(), 2);
135-        assert_eq!(all[0].session_id, "s1");
136-        assert_eq!(all[0].trigger, SessionTrigger::Boot);
137-        assert_eq!(all[1].session_id, "s2");
138-        assert_eq!(all[1].trigger, SessionTrigger::Clear);
139-    }
140-
141-    // [unit->REQ-TERM-6] a re-bind of the same session is deduped (not a new
142-    // boundary); a different session appends.


crates\spt\tests\live_bind_firsthost_e2e.rs
crates\spt\tests\live_firsthost_e2e.rs


Dedup confirmed (`append` L99-100: last-row same-session → `Ok(false)`, no dupe). Impl comment accurate.

Now verify the positive counterpart proves hosting with same manifest shape:


1	//! M11-W0.3 first-host int (REQ-DAEMON-1) — the spt-hosted (bind, no listen)
2	//! mirror of `live_firsthost_e2e`, closing the bind-no-Psyche gap:
3	//!
4	//! a live-capable `spt api bind <id> --type live_agent` BINDS the Self perch
5	//! (direct-PTY topology, no relay — CONTEXT:39) and marks it `status=online` (the
6	//! W0.3 establish-marks-online, mirror of W0.2's listen mark) → the brain's
7	//! `livehost::reconcile_once` then HOSTS the Psyche off that real online status.
8	//! Before W0.3 a bind-only spt-hosted live agent never got a Psyche (the Psyche
9	//! spawn was cmd_listen-only); this proves it now does — through the REAL bind,
10	//! not a hand-seeded status.
11	//
12	// [int->REQ-DAEMON-1]
13
14	use std::path::PathBuf;
15	use std::process::{Command, Output};
16	use std::sync::Arc;
17	use std::time::Duration;
18
19	mod common;
20	use common::CommandNoWindowExt;
21
22	use spt_daemon::brainproc::StartReason;
23	use spt_daemon::config::DaemonConfig;
24	use spt_daemon::livehost::{self, LiveSet};
25	use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};
26	use spt_store::perch::{self, ParentHint};
27
28	#[cfg(windows)]
29	const NOOP: &str = "cmd /C rem";
30	#[cfg(unix)]
31	const NOOP: &str = "true";
32
33	fn start_inproc_daemon() {
34	    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
35	    let name = spt_daemon::seed_socket_name();
36	    std::thread::spawn(move || {
37	        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);
38	    });
39	    for _ in 0..400 {
40	        if spt_daemon::is_running() {
41	            return;
42	        }
43	        std::thread::sleep(Duration::from_millis(5));
44	    }
45	    panic!("in-process seed daemon did not come up");
46	}
47
48	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
49	    let (tx, rx) = std::sync::mpsc::channel();
50	    std::thread::spawn(move || {
51	        let _ = tx.send(cmd.output());
52	    });
53	    rx.recv_timeout(deadline)
54	        .expect("captured spt call must complete")
55	        .expect("run spt")
56	}
57
58	#[test]
59	fn live_bind_marks_online_and_brain_reconcile_hosts() {
60	    let home = tempfile::tempdir().unwrap();
61	    std::env::set_var("SPT_HOME", home.path());
62	    start_inproc_daemon();
63
64	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
65
66	    // A live-capable adapter (declares [session.psyche_init]) + register it so the
67	    // brain reconcile resolves the manifest from disk by the perch's adapter.
68	    let drops = home.path().join("drops");
69	    std::fs::create_dir_all(&drops).unwrap();
70	    let dir = drops.to_string_lossy().replace('\\', "/");
71	    let manifest_toml = format!(
72	        "[adapter]\nname=\"mocklive\"\nkind=\"harness\"\nversion=\"1\"\n\
73	         min_spt_core_version=\"0\"\n\n\
74	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
75	         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
76	    );
77	    let manifest_path = home.path().join("mocklive.toml");
78	    std::fs::write(&manifest_path, &manifest_toml).unwrap();
79	    let src = perch::spt_home().join("srcs").join("mocklive");
80	    std::fs::create_dir_all(&src).unwrap();
81	    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
82	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
83
84	    let mp = manifest_path.to_string_lossy().to_string();
85	    let spt = |args: &[&str]| {
86	        let mut cmd = Command::new(&spt_bin);
87	        cmd.no_window().args(args).env("SPT_HOME", home.path());
88	        output_bounded(cmd, Duration::from_secs(60))
89	    };
90
91	    // ── the REAL spt-hosted bind (no seed, no listen): establishes the Self perch
92	    // and marks it online (the W0.3 first-host handoff). The Psyche does NOT spawn
93	    // in this process — bind only writes the online signal.
94	    let out = spt(&[
95	        "api", "--adapter", "mocklive", "--manifest", &mp, "bind", "agent8",
96	        "--type", "live_agent", "--set-session-id", "sid-1",
97	    ]);
98	    assert!(out.status.success(), "bind: {}", String::from_utf8_lossy(&out.stderr));
99
100	    // The real establish-marks-online: written by cmd_bind, NOT hand-seeded.
101	    let self_perch = perch::resolve_perch_path("agent8", ParentHint::Infer);
102	    assert_eq!(
103	        spt_store::info::read_info(&self_perch)
104	            .and_then(|i| i.status)
105	            .as_deref(),
106	        Some(STATUS_ONLINE),
107	        "a live-capable `api bind` marks the perch online (the spt-hosted first-host handoff)"
108	    );
109
110	    // ── THE GAP CLOSES: the brain reconcile hosts the Psyche for the bind-only
111	    // spt-hosted live agent — it never did before W0.3 (spawn_psyche was
112	    // cmd_listen-only). FAST pulse: the driver hammers set_last_active every tick;
113	    // the W0.4 mutate_info sentinel lock makes the offline flip below survive that
114	    // contention (no long-pulse+settle workaround needed).
115	    let set = Arc::new(LiveSet::new());
116	    let cfg = DaemonConfig {
117	        pulse_period: Duration::from_millis(15),
118	        ..DaemonConfig::default()
119	    };
120	    let registered = spt_runtime::registry::registered(&perch::adapters_dir());
121	    livehost::reconcile_once(
122	        &perch::owlery_dir(),
123	        &registered,
124	        &perch::adapters_dir(),
125	        &set,
126	        &cfg,
127	        StartReason::Cold,
128	    );
129	    assert_eq!(
130	        set.len(),
131	        1,
132	        "the brain hosts the Psyche for the bind-established live agent (bind-no-Psyche gap closed)"
133	    );
134	    // The Psyche nested perch was created by the brain host (daemon online-stamp).
135	    let psyche_perch = perch::resolve_perch_path("agent8-psyche", ParentHint::Explicit("agent8"));
136	    assert!(
137	        spt_store::info::read_info(&psyche_perch).is_some(),
138	        "the brain spawned the Psyche nested perch"
139	    );
140
141	    // ── teardown via the public un-host path (offline transition). No settle: the
142	    // W0.4 sentinel lock makes the offline flip survive the driver's concurrent
143	    // per-tick set_last_active.
144	    spt_store::info::set_status(&self_perch, STATUS_OFFLINE).unwrap();
145	    livehost::reconcile_once(
146	        &perch::owlery_dir(),
147	        &registered,
148	        &perch::adapters_dir(),
149	        &set,
150	        &cfg,
151	        StartReason::Cold,
152	    );
153	    assert!(set.is_empty(), "an offline transition un-hosts the lifecycle");
154
155	    // Reap any REAL daemon that auto-started against this throwaway home (hygiene).
156	    if let Ok(p) = std::fs::read_to_string(home.path().join("daemon.pid")) {
157	        if let Ok(p) = p.trim().parse::<u32>() {
158	            #[cfg(windows)]
159	            let _ = Command::new("taskkill")
160	                .no_window()
161	                .args(["/PID", &p.to_string(), "/F"])
162	                .output();
163	            #[cfg(unix)]
164	            let _ = Command::new("kill").args(["-9", &p.to_string()]).output();
165	        }
166	    }
167	}
168

//! M11-W0.3 first-host int (REQ-DAEMON-1) — the spt-hosted (bind, no listen)
//! mirror of `live_firsthost_e2e`, closing the bind-no-Psyche gap:
//!
//! a live-capable `spt api bind <id> --type live_agent` BINDS the Self perch
//! (direct-PTY topology, no relay — CONTEXT:39) and marks it `status=online` (the
//! W0.3 establish-marks-online, mirror of W0.2's listen mark) → the brain's
//! `livehost::reconcile_once` then HOSTS the Psyche off that real online status.
//! Before W0.3 a bind-only spt-hosted live agent never got a Psyche (the Psyche
//! spawn was cmd_listen-only); this proves it now does — through the REAL bind,
//! not a hand-seeded status.
//
// [int->REQ-DAEMON-1]

use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::Arc;
use std::time::Duration;

mod common;
use common::CommandNoWindowExt;

use spt_daemon::brainproc::StartReason;
use spt_daemon::config::DaemonConfig;
use spt_daemon::livehost::{self, LiveSet};
use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};
use spt_store::perch::{self, ParentHint};

#[cfg(windows)]
const NOOP: &str = "cmd /C rem";
#[cfg(unix)]
const NOOP: &str = "true";

fn start_inproc_daemon() {
    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
    let name = spt_daemon::seed_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);
    });
    for _ in 0..400 {
        if spt_daemon::is_running() {
            return;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    panic!("in-process seed daemon did not come up");
}

fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete")
        .expect("run spt")
}

#[test]
fn live_bind_marks_online_and_brain_reconcile_hosts() {
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));

    // A live-capable adapter (declares [session.psyche_init]) + register it so the
    // brain reconcile resolves the manifest from disk by the perch's adapter.
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"mocklive\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='{NOOP}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    let manifest_path = home.path().join("mocklive.toml");
    std::fs::write(&manifest_path, &manifest_toml).unwrap();
    let src = perch::spt_home().join("srcs").join("mocklive");
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    let mp = manifest_path.to_string_lossy().to_string();
    let spt = |args: &[&str]| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(60))
    };

    // ── the REAL spt-hosted bind (no seed, no listen): establishes the Self perch
    // and marks it online (the W0.3 first-host handoff). The Psyche does NOT spawn
    // in this process — bind only writes the online signal.
    let out = spt(&[
        "api", "--adapter", "mocklive", "--manifest", &mp, "bind", "agent8",
        "--type", "live_agent", "--set-session-id", "sid-1",
    ]);
    assert!(out.status.success(), "bind: {}", String::from_utf8_lossy(&out.stderr));

    // The real establish-marks-online: written by cmd_bind, NOT hand-seeded.
    let self_perch = perch::resolve_perch_path("agent8", ParentHint::Infer);
    assert_eq!(
        spt_store::info::read_info(&self_perch)
            .and_then(|i| i.status)
            .as_deref(),
        Some(STATUS_ONLINE),
        "a live-capable `api bind` marks the perch online (the spt-hosted first-host handoff)"
    );

    // ── THE GAP CLOSES: the brain reconcile hosts the Psyche for the bind-only
    // spt-hosted live agent — it never did before W0.3 (spawn_psyche was
    // cmd_listen-only). FAST pulse: the driver hammers set_last_active every tick;
    // the W0.4 mutate_info sentinel lock makes the offline flip below survive that
    // contention (no long-pulse+settle workaround needed).
    let set = Arc::new(LiveSet::new());
    let cfg = DaemonConfig {
        pulse_period: Duration::from_millis(15),
        ..DaemonConfig::default()
    };
    let registered = spt_runtime::registry::registered(&perch::adapters_dir());
    livehost::reconcile_once(
        &perch::owlery_dir(),
        &registered,
        &perch::adapters_dir(),
        &set,
        &cfg,
        StartReason::Cold,
    );
    assert_eq!(
        set.len(),
        1,
        "the brain hosts the Psyche for the bind-established live agent (bind-no-Psyche gap closed)"
    );
    // The Psyche nested perch was created by the brain host (daemon online-stamp).
    let psyche_perch = perch::resolve_perch_path("agent8-psyche", ParentHint::Explicit("agent8"));
    assert!(
        spt_store::info::read_info(&psyche_perch).is_some(),
        "the brain spawned the Psyche nested perch"
    );

    // ── teardown via the public un-host path (offline transition). No settle: the
    // W0.4 sentinel lock makes the offline flip survive the driver's concurrent
    // per-tick set_last_active.
    spt_store::info::set_status(&self_perch, STATUS_OFFLINE).unwrap();
    livehost::reconcile_once(
        &perch::owlery_dir(),
        &registered,
        &perch::adapters_dir(),
        &set,
        &cfg,
        StartReason::Cold,
    );
    assert!(set.is_empty(), "an offline transition un-hosts the lifecycle");

    // Reap any REAL daemon that auto-started against this throwaway home (hygiene).
    if let Ok(p) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(p) = p.trim().parse::<u32>() {
            #[cfg(windows)]
            let _ = Command::new("taskkill")
                .no_window()
                .args(["/PID", &p.to_string(), "/F"])
                .output();
            #[cfg(unix)]
            let _ = Command::new("kill").args(["-9", &p.to_string()]).output();
        }
    }
}


Positive counterpart confirmed: `live_bind_marks_online_and_brain_reconcile_hosts` uses **identical** manifest (NOOP `psyche_init`, registered mocklive) + identical `reconcile_once` call, asserts `set.len()==1` for a `live_agent`-state perch. My negative int inverts exactly that — only diff is `state=ready_agent` → `hosted=0`. **Non-vacuous** ✓.

Design verdict so far: clean. Now independently run new unit + int + the positive anchor in `target-seam`.


Bash

CARGO_TARGET_DIR=target-seam cargo test -p spt-msg --lib start_ledgers_a_boot_session_row -- --exact --nocapture 2>&1 | tail -25

Run new ready-ledger unit test in target-seam


Command running in background with ID: bnc2vmv9y. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bnc2vmv9y.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bnc2vmv9y</task-id>
<tool-use-id>toolu_01DtDeVjsU3iyBfkHovPjdid</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bnc2vmv9y.output</output-file>
<status>completed</status>
<summary>Background command "Run new ready-ledger unit test in target-seam" completed (exit code 0)</summary>
</task-notification>


Unit running in target-seam (background). Waiting — int + positive anchor next (serialize to avoid target-dir lock).


until grep -qE "test result:|error\[|error:" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bnc2vmv9y.output" 2>/dev/null; do sleep 5; done; echo "---DONE---"; tail -15 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bnc2vmv9y.output"

Wait for unit test completion


---DONE---
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests src\lib.rs (target-seam\debug\deps\spt_msg-d9eb919d15b04ab3.exe)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 32 filtered out; finished in 0.00s


Prompt:
<task-notification>
<task-id>bnc2vmv9y</task-id>
<tool-use-id>toolu_01DtDeVjsU3iyBfkHovPjdid</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bnc2vmv9y.output</output-file>
<status>completed</status>
<summary>Background command "Run new ready-ledger unit test in target-seam" completed (exit code 0)</summary>
</task-notification>


`--exact` matched 0 — needs full module path. Re-run without `--exact`:


CARGO_TARGET_DIR=target-seam cargo test -p spt-msg --lib start_ledgers_a_boot_session_row -- --nocapture 2>&1 | tail -12

Re-run ready-ledger unit test without exact filter


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running unittests src\lib.rs (target-seam\debug\deps\spt_msg-d9eb919d15b04ab3.exe)

running 1 test
test ready::tests::start_ledgers_a_boot_session_row ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 31 filtered out; finished in 0.10s


Unit ✓ (`ready::tests::start_ledgers_a_boot_session_row`). Now int + positive anchor:


CARGO_TARGET_DIR=target-seam cargo test -p spt --test ready_resume_ledger_e2e -- --nocapture 2>&1 | tail -30

Run ready-resume int e2e in target-seam


Command running in background with ID: b5auo8pxy. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\b5auo8pxy.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>b5auo8pxy</task-id>
<tool-use-id>toolu_013CMtLikpCZWHtDNs1zsKHW</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\b5auo8pxy.output</output-file>
<status>completed</status>
<summary>Background command "Run ready-resume int e2e in target-seam" completed (exit code 0)</summary>
</task-notification>


until grep -qE "test result:|error\[|error:|panicked" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\b5auo8pxy.output" 2>/dev/null; do sleep 5; done; echo "---DONE---"; tail -30 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\b5auo8pxy.output"

Wait for int e2e completion


---DONE---
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.05s
     Running tests\ready_resume_ledger_e2e.rs (target-seam\debug\deps\ready_resume_ledger_e2e-f39eb5124fbda1e7.exe)

running 1 test
=== DIAGNOSTIC: ledger_rows=[SessionEntry { ts: "2026-06-18T11:06:31Z", session_id: "sess-resume-carry-42", trigger: Boot }] state=Some("ready_agent") hosted=0 psyche_appeared=false ===
test ready_bind_ledgers_and_reconcile_hosts_no_psyche ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s


Prompt:
<task-notification>
<task-id>b5auo8pxy</task-id>
<tool-use-id>toolu_013CMtLikpCZWHtDNs1zsKHW</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\b5auo8pxy.output</output-file>
<status>completed</status>
<summary>Background command "Run ready-resume int e2e in target-seam" completed (exit code 0)</summary>
</task-notification>


Int ✓. Diagnostic proves non-vacuous: one Boot row carrying exact session `sess-resume-carry-42`, `state=ready_agent`, `hosted=0`, `psyche_appeared=false`.

Now: positive anchor (my non-vacuity baseline) + traceable check.


CARGO_TARGET_DIR=target-seam cargo test -p spt --test live_bind_firsthost_e2e 2>&1 | tail -8

Run positive anchor live_bind_firsthost


Command running in background with ID: bvnxz7x4n. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bvnxz7x4n.output. You will be notified when it completes. To check interim output, use Read on that file path.


traceable-reqs check 2>&1 | tail -20; echo "EXIT=$?"

Run traceable-reqs check


  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
  [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).
EXIT=0


<task-notification>
<task-id>bvnxz7x4n</task-id>
<tool-use-id>toolu_01HUw5U9qcxVCuVLjnUMsCjW</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bvnxz7x4n.output</output-file>
<status>completed</status>
<summary>Background command "Run positive anchor live_bind_firsthost" completed (exit code 0)</summary>
</task-notification>


traceable EXIT=0. Waiting on positive anchor:


until grep -qE "test result:|error\[|error:|panicked" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bvnxz7x4n.output" 2>/dev/null; do sleep 5; done; echo "---DONE---"; tail -10 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bvnxz7x4n.output"

Wait for positive anchor completion


---DONE---
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.56s
     Running tests\live_bind_firsthost_e2e.rs (target-seam\debug\deps\live_bind_firsthost_e2e-46cc4666dbc1d414.exe)

running 1 test
test live_bind_marks_online_and_brain_reconcile_hosts ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.19s


Prompt:
<task-notification>
<task-id>bvnxz7x4n</task-id>
<tool-use-id>toolu_01HUw5U9qcxVCuVLjnUMsCjW</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\9d0930b3-34c1-4dce-943f-72ccdc8fb726\tasks\bvnxz7x4n.output</output-file>
<status>completed</status>
<summary>Background command "Run positive anchor live_bind_firsthost" completed (exit code 0)</summary>
</task-notification>


All gate checks green:

| Check | Result |
|---|---|
| Code-read diff @464eaf7 | clean, targeted (+251, 4 files) |
| `append` dedup (re-bind safety) | confirmed L99-100 |
| unit `start_ledgers_a_boot_session_row` | ✓ |
| int `ready_bind_ledgers_and_reconcile_hosts_no_psyche` | ✓ non-vacuous (diag: 1 Boot row=carried session, state=ready_agent, hosted=0) |
| positive anchor `live_bind_marks_online...hosts` | ✓ (same NOOP psyche_init → hosts for live state) |
| traceable check | EXIT=0 |
| FIX(2) SUBSUMED state-gate L319 | verified-at-code |

**GATE-PASS.** Last v0.12.0 item. Reply todlando + line up release.


Skill

todlando GATE-PASS — REQ-READY-AGENT-RESUME @464eaf7. Independently verified: read full diff; confirmed sessions::append dedups same-session re-bind (L99-100), so the Boot row is re-bind-safe. Ran in target-seam: unit start_ledgers_a_boot_session_row ✓, int ready_bind_ledgers_and_reconcile_hosts_no_psyche ✓ (diagnostic non-vacuous: one Boot row carrying the exact carried session sess-resume-carry-42, state=ready_agent, hosted=0, psyche_appeared=false), positive anchor live_bind_marks_online_and_brain_reconcile_hosts ✓ (same NOOP psyche_init adapter DOES host for live_agent state — proves the negative isolates the START-side state gate). traceable check EXIT=0. FIX(2) SUBSUMED accepted: verified-at-code, state gate (livehost.rs:319 info.state != LIVE_AGENT_STATE) fires before adapter/psyche_init gates — zero code, exactly the operator's "type = manifest concern" scoping. CONTEXT.md doc accurate. This closes the LAST v0.12.0 item. You are CLEAR. Next: I line up deployah for the v0.12.0 MINOR release pending operator GO (stack is 11 ahead of main, unpushed). REQ-PICKER-ADAPTER-DESCRIPTION stays deferred fast-follow.


Launching skill: spt:send


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

# /spt:send

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

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

## Messaging Command Reference

Both commands read the message body from **stdin** (pipe or heredoc).

```bash
# send -- fire-and-forget to a target (use when you have your own perch)
$OWL send <target> <<'EOF'
message body
EOF

# send --reply-to -- respond to whoever messaged you
$OWL send --reply-to <sender> [your-id] <<'EOF'
response body
EOF

# ask -- deliver + create ephemeral reply perch + poll for reply (use when you have NO perch)
$OWL ask <target> [from] <<'EOF'
message body
EOF
```

If auto-detection fails, pass your ID explicitly: `$OWL send <target> <your-id>`, `$OWL send --reply-to <sender> <your-id>`, `$OWL ask <target> <impromptu-id>`.

**When to use which:**
- **`send`**: You already have a listener. Fire-and-forget, no reply perch created.
- **`send --reply-to`**: You received a message and want to respond. `from` is auto-set from the original sender.
- **`ask`**: You have no listener. Creates a temporary perch, delivers, then polls for the reply.

## Pre-checks

- If `<target>` matches this agent's own listener ID, refuse. Self-sends create loops.
- All send/reply-to/ask commands verify the target is alive automatically.

## If you already have a listener

Just send -- no reply perch needed. Run **not** in background:

```bash
$OWL send <target> <<'EOF'
<msg>
EOF
```

## If you have no listener

The `ask` subcommand creates an ephemeral reply perch, delivers, and polls for the reply. Run with `run_in_background: true` (or `false` for `--block`):

```bash
$OWL ask <target> <<'EOF'
<msg>
EOF
```

## On response

**Important:** If you have an active listener perch, always launch Agent tool calls with `run_in_background: true`. Foreground agents block your poll loop -- no messages (including replies to your send) can be delivered until the agent finishes.

When the response arrives, present it to the user. Responses arrive via one of three paths:
- **Background interject** -- the `$OWL ask` background task completes with the reply.
- **Inline** -- if you used `--block`, the reply prints directly.
- **Hook-injected XML** -- if you are mid-tool-call, the reply appears as `<owl_messages>` XML in your tool call context. The `from` attribute is the sender.

ARGUMENTS: todlando GATE-PASS — REQ-READY-AGENT-RESUME @464eaf7. Independently verified: read full diff; confirmed sessions::append dedups same-session re-bind (L99-100), so the Boot row is re-bind-safe. Ran in target-seam: unit start_ledgers_a_boot_session_row ✓, int ready_bind_ledgers_and_reconcile_hosts_no_psyche ✓ (diagnostic non-vacuous: one Boot row carrying the exact carried session sess-resume-carry-42, state=ready_agent, hosted=0, psyche_appeared=false), positive anchor live_bind_marks_online_and_brain_reconcile_hosts ✓ (same NOOP psyche_init adapter DOES host for live_agent state — proves the negative isolates the START-side state gate). traceable check EXIT=0. FIX(2) SUBSUMED accepted: verified-at-code, state gate (livehost.rs:319 info.state \!= LIVE_AGENT_STATE) fires before adapter/psyche_init gates — zero code, exactly the operator's "type = manifest concern" scoping. CONTEXT.md doc accurate. This closes the LAST v0.12.0 item. You are CLEAR. Next: I line up deployah for the v0.12.0 MINOR release pending operator GO (stack is 11 ahead of main, unpushed). REQ-PICKER-ADAPTER-DESCRIPTION stays deferred fast-follow.


Bash

$OWL send todlando <<'EOF'
GATE-PASS — REQ-READY-AGENT-RESUME @464eaf7. Independently verified.

Read full diff. Confirmed sessions::append dedups same-session re-bind (L99-100), so the Boot row is re-bind-safe. Ran in target-seam:
- unit start_ledgers_a_boot_session_row OK
- int ready_bind_ledgers_and_reconcile_hosts_no_psyche OK (diagnostic non-vacuous: one Boot row carrying the exact carried session sess-resume-carry-42, state=ready_agent, hosted=0, psyche_appeared=false)
- positive anchor live_bind_marks_online_and_brain_reconcile_hosts OK (same NOOP psyche_init adapter DOES host for live_agent state -- proves the negative isolates the START-side state gate)
- traceable check EXIT=0

FIX(2) SUBSUMED accepted: verified-at-code, state gate (livehost.rs:319 info.state != LIVE_AGENT_STATE) fires before the adapter/psyche_init gates -- zero code, exactly the operator's "type = manifest concern" scoping. CONTEXT.md doc accurate.

This closes the LAST v0.12.0 item. You are CLEAR. Next: I line up deployah for the v0.12.0 MINOR release pending operator GO (stack is 11 ahead of main, unpushed). REQ-PICKER-ADAPTER-DESCRIPTION stays deferred fast-follow.
EOF

Send GATE-PASS verdict to todlando


➜ SENT:todlando


Sent. Now record state + surface release decision. v0.12.0 publish is outward-facing/irreversible (signed public release) — all prior releases had explicit operator GO, and there's no active operator turn. Will NOT auto-publish; line it up and request GO.

Update milestone memory:


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 STATE @464eaf7 (NOT pushed):** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap = ALL GATE-PASS. endpoint-purge @981dea2 = **doyle GATE-PASS** (he ran unit+int himself). **ready-agent-resume @464eaf7 = BUILT seam-green, AWAITING doyle gate (LAST item).** On gate-PASS → deployah → v0.12.0 MINOR. doyle communing + operator ending session 2026-06-18; doyle gates my seam-green ping first thing next session.
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).
16	- **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.
17
18	**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.**
19	- **REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP** @909ad01 [impl,unit,int]: brain restart left DUPLICATE psyche/endpoint (abrupt brain death → stop_host never runs, old psyche orphaned alive; new brain re-hosts + overwrites perch pid → old untracked til daemon-stop). FIX = reap_orphan_psyches at brain-start (spawn_live_host, BEFORE first reconcile): triple-gate psyche_orphan_should_reap = alive AND normalize_basename(exe)==adapter psyche prog AND process_cmdline(pid).contains("<id>-psyche"). **ID-SPECIFIC is load-bearing (doyle): basename-only WRONG-KILLS a sibling — all sibling agents share `claude` basename; a recycled pid on a sibling's claude matches basename alone. cmdline-contains-full-"<id>-psyche" is sibling+prefix-safe; FAILS SAFE (any None→decline; missed dup Breap-bounded, wrong-kill catastrophic).** CAVEAT (documented): cmdline carries id only if psyche_init bakes {id} (norm); non-{id}=safe-miss. NO start-time (doyle YAGNI). New spt_store::proc::{kill_pid scoped, process_cmdline (Win NtQueryInformationProcess class60 UNICODE_STRING / Linux /proc/cmdline)}. unit orphan_reap_..._spares_a_same_basename_sibling + process_cmdline_reads_a_live_arg_marker (Win FFI in-test). int brain_restart_leaves_exactly_one_psyche (real daemon, SINGLE-pid brain kill→respawn→P1 reaped+one P2).
20	- **B4 CLOSED SUBSUMED** (REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE, required_stages=[]): 5-axis evidence all-rebuilt-on-restart (config→with_config_in / pulse+psyche→spawn_live_host@230→reconcile_once / delivery→resume_sessions@797 / online-off→B2+B5 / shellwake→spawn_wake_host@219). Kept the id (not redefined) — the real residual = dup-reap (own id). doyle "win not miss".
21
22	**(prior wave-5 first half) psyche-reap @7449f5e GATE-PASS** — owned-Child reap (recycle-PROOF). B4 = axis-table SUBSUMED.
23	- **REQ-HAZARD-UNHOST-PSYCHE-REAP** @7449f5e [impl,unit,int]: on un-host (endpoint-stop/signoff/B2+B5-offline) stop_host joined the driver THREAD but never killed the detached {id}-psyche PROCESS → orphan til daemon-stop/Breap. FIX = OWNED-Child (doyle mandate, recycle-PROOF vs bare-pid hitting a recycled sibling on shared box): additive spawn_session_owned→OwnedSession{pid,Child} at every layer (spt-runtime→spt-live spawn_and_bind_owned/spawn_psyche_owned→BrainLifecycle::spawn_psyche_owned→HostedLife.psyche_child→stop_host child.kill()+wait()). Fire-and-forget callers untouched (spawn_session=owned-then-drop). kill_pid REMOVED (owned path = dead-code). **GOTCHA #1 STRUCK AGAIN (seam caught, not code-read): owning the handle made confirm_residency_or_unhost's is_process_alive(perch_pid) read a fast-EXITED psyche ALIVE on Win (held handle lingers til wait()) → masked F-010 → nonresident E2E RED → FIX: residency probes owned child via try_wait (LiveSet::psyche_running). bootrace(resident) green throughout; only fast-exit caught it.** Owned-Child also = the recycle-proof pattern for any long-lived hosted child (sibling of Breap's owned brain).
24	- **B4 REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE = SUBSUMED (axis table all-covered):** BrainLifecycle (lifecycle.rs:61-74) is PURE CONFIG. On bare brain restart every axis already rebuilt: config→host_one with_config_in; pulse+psyche-host→run_brain spawn_live_host(:230)→reconcile_once; PTY-delivery cursors→resume_sessions(:797); online/offline→B2+B5; shellwake→spawn_wake_host(:219). REQ-as-written ("no livehost / Psyche never re-hosted") is STALE (predates the reconcile). → close as covered (doyle "win not miss"). **NEW RESIDUAL surfaced (high-confidence reasoning, untested): brain restart ORPHANS old psyches — stop_host never runs on abrupt brain death (LiveSet+owned handles die with brain); Breap job/group only fires at DAEMON stop; new brain re-hosts FRESH + overwrites {id}-psyche perch pid → OLD psyche untracked+alive = DUPLICATE psyche/endpoint til daemon-stop. Likely perri's "brain kill+restart wedged everything." REAP gap, not rehydrate. FIX = on brain start reap pre-existing {id}-psyche orphan (UNOWNED kill_pid back WITH a use + recycle guard is_process_alive+exe_basename). doyle ruling pending: (a) close B4 + mint REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP, (b) redefine B4, (c) defer to Breap-bound. I lean a/b.**
25
26	**PRIOR PROGRESS: B2✓ @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e. WAVE 4 (B5) GATE-PASS @9e338f1 (doyle INDEPENDENTLY ran the runtime sweep — clippy/traceable EXIT=0, B5 unit + ALL 6 livehost E2Es incl new bootgate green, bind-firsthost still hosts = no false-offline regression). WAVE 5 (LAST) RELEASED to todlando = B4 + NEW REQ-HAZARD-UNHOST-PSYCHE-REAP.** Then deployah → v0.12.0 MINOR.
27
28	**PSYCHE-IS-A-PROCESS RULING (operator Q, grounded CONTEXT.md+code):** the Psyche is a HARNESS PROCESS by design — a headless `claude --resume` session (CONTEXT.md:97; :203 own independent perch; :251 agent endpoint "something intelligent runs there"; code: manifest `[session.psyche_init/resume]` command, turn.rs:25-27 "persistent harness session"). What lives IN the brain as a THREAD is the pulse/psyche-driver LOOP (CONTEXT.md:34 "pulse/psyche loops"; = HostedLife.thread, joined by stop_host). spt-core moved away from legacy's separate WRAPPER (CONTEXT.md:173 "no separate wrapper"), NOT the psyche process. So the orphan fix is design-consistent: un-host must KILL the harness psyche process (NOT move it in-brain). → NEW REQ-HAZARD-UNHOST-PSYCHE-REAP (wave 5 w/ B4): stop_host kills the {id}-psyche pid (on the perch, residency reads it), scoped. [impl,unit,int].
29
30	**WAVE 4 (B5) design [impl,unit,int]:** lifted LIVENESS_RECONCILE_BOOT_GRACE skip in spawn_live_host → B2's reconcile_hosted_liveness runs from BOOT tick 1 (BEFORE reconcile_once) → sessionless controllable==Some(true) perch OFFLINED at boot → reconcile_once skips host → no phantom revival. doyle: REUSE the B2 fn, no 2nd liveness notion. WHY grace-drop is safe: an spt-hosted agent runs IN a broker PTY so its session PRECEDES the online mark → online+controllable+sessionless = ONLY a cold-start phantom (offline it) OR brain-restart survivor (in the session set, kept). None-skip + controllable gate preserved. 3-WAY MATRIX: (i)child-dead-midlife→B2, (ii)boot-revive→B5, (iii)dead-{id}-psyche→residency-confirm. NO E2E regression: existing livehost E2Es use `api listen` (Some(false), exempt) OR call reconcile_once in-proc w/o broker (None-skip); none host a sessionless-controllable perch. unit boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host; int livehost_bootgate_e2e (real cold-start, stale online+controllable perch no session → offlined at boot + no {id}-psyche). **OPEN (doyle→operator scope call): stop_host stops the DRIVER thread but NOT the detached psyche PROCESS — a mid-life endpoint-death/stop leaves the psyche running until daemon-stop/Breap. Real+production-reachable, separate from B5/B4. Candidate new REQ (stop_host reads {id}-psyche pid + kills it). Not in B5/B4 unless operator greenlights.**
31
32	**WAVE 3 design (B3+Breap), both [impl,unit]:** B3 = client-side barrier in seedmap::request_stop (after STOPPING ack, drop conn, poll ping() to failure / 10ms slices, 5s wedge-ceiling — doyle's option 2, NOT socket-unbind-before-ack). Breap = `reap::BrainReaper` rooted at the BRAIN SUBTREE (never the daemon → reap never self-terminates, never touches a sibling live agent on the shared box): Win = kill-on-job-close Job, brain assigned per (re)spawn in spawn_brain_supervisor, Psyches inherit; Unix = brain pre_exec setpgid(0,0) leads own group, supervisor records pgid, kill(-pgid,SIGKILL). Daemon::run graceful path raises supervisor stop flag FIRST (hoisted the "held for symmetry never raised" Arc) THEN reaps; straggler-brain backstop = self-exit on broker-loss.
33
34	**WAVE 3 LESSONS (2 Windows-process gotchas, both reusable):** (1) **terminated process w/ open handle reads ALIVE** — is_process_alive(pid) (OpenProcess) returns true for a TerminateJobObject'd process while a Child handle is still held (kernel object lingers; same class as the Unix-zombie OpenProcess gap). For an OWNED child use try_wait, not is_process_alive(pid). (2) **UseShellExecute=true → job breakaway** — Windows PowerShell's [Diagnostics.Process]::Start defaults UseShellExecute=true → ShellExecute spawns the grandchild OUTSIDE the job → it survives the reap. Force UseShellExecute=$false (CreateProcess) so it inherits the job — mirrors the brain→Psyche spawn (null stdio + CreateProcess, no breakaway). doyle's "exercise the grandchild link, don't reason it" caught this — the child-only unit was green while the real orphan path escaped.
35
36	**CRITICAL LESSON — B1 path-(c) deadlock (cost ~3h, gate code-read MISSED it):** f85bf69's "primary dead-detector" `if let Ok(Some)=session.try_wait()` in Broker::dispatch_subscribe DEADLOCKS every LIVE attach (incl production rc). `PtySession::try_wait` locks the child mutex; the per-session exit-waiter thread holds that SAME mutex in a blocking `wait()` for the WHOLE life of a live child (broker.rs exit-waiter, pty.rs:203). Live child → try_wait blocks forever → no Subscribed reply → hang. Broker units passed (their children exit fast → lock freed); only the FULL attach suite (matrix/loopback/remote, 85-200s hangs) caught it. **Gating data-flow logic ("try_wait Some→Exit; None→live-safe") is BLIND to lock-contention/ordering deadlocks — you MUST run the real runtime sweep, not just touched units + a code-read.** Sibling of [[shared-seam-change-run-all-seam-tests]]. Path-(c) was also redundant: exit-waiter already broadcasts Exit to all sinks on reap; post-removal subscribe → "no such session". RED HERRINGS chased first: "concurrent cargo wedge" (real but not this), "input-race needs ready-banner" (phantom — silent-child tests pass in CI), "environmental QUIC-under-load" (wrong — cwd passed instantly). Operator's "fix it, don't tape it" forced the real root.
37
38	**7 REQs minted registry-first @e2d6391 (traceable EXIT=0). WAVE ORDER: B2 → B1+H3 → B3+Breap → B5 → B4.**
39	- **B2 REQ-HAZARD-HOSTED-LIVENESS-RECONCILE** (keystone) — **BUILT @03b6398, WAVE-READY, awaiting doyle gate.** doyle ruled PULL-PRIMARY (live-status analog of v0.11.0 ROSTER-GHOST). new `reconcile_hosted_liveness(owlery, live_sessions)` in livehost.rs: queries broker KIND_SESSIONS each tick (`query_live_session_endpoints`), marks_offline any status=online live_agent perch with NO live broker session. Wired into spawn_live_host BEFORE reconcile_once, gated past-10s-boot-grace + broker-reachable (None→skip). **CONTROLLABLE GATE (load-bearing): only controllable==Some(true) spt-hosted; relay Some(false) + legacy None EXEMPT** (else kills perri /sptc:live agents). PUSH (ExitEvent→mark_offline) NOT added — confirmed daemon brain has NO event-consumer loop (main=net_status heartbeat), push has nowhere to land for unattended; correctness rides pull. unit pull_liveness_marks_sessionless_spt_hosted_offline_only + int pull_reconcile_offlines_perch_when_broker_session_dies (real broker session killed+reaped→latch cleared). [impl,unit,int].
40	- **B1 REQ-HAZARD-RC-ATTACH-FAILFAST** — rc to dead/silent session → infinite blank. SUB-MECHANISM PINNED: wall-f hang = broker STILL resolves a session (resolve_session rc.rs:209 returns Some — child alive-silent OR dead-not-reaped on Win tab-close) → pump blocks on first read that never comes. FIX (doyle): (a) gate attach on is_online/status [after B2], (b) fail-fast bound first-output/ack→clear msg, (c) broker EOF stream on dead-child→PumpEnd::BrokerGone (REQ-HAZARD-RC-EOF). PIN with repro test FIRST (deterministic = attach to alive-but-silent session). wave 2.
41	- **H3 REQ-ENDPOINT-STOP-OFFLINE** — cmd_stop (cli.rs:2994-3010) removes ready+unregister but NO set_status offline → stopped still alive=true. FIX: add set_status(perch, STATUS_OFFLINE), folds B2 setter. CONFIRMED at code. wave 2.
42	- **B3 REQ-HAZARD-DAEMON-STOP-BARRIER** — request_stop (seedmap.rs:240-255) returns on KIND_STOPPING ack (174-176) BEFORE seed socket unbinds → is_running ping (daemon.rs:375) wins → start sees ALREADY_RUNNING. FIX: unbind before ack OR wait-ping-fail. wave 3.
43	- **Breap REQ-HAZARD-DAEMON-STOP-REAP** — stop leaves ~8 orphaned psyche/spt.exe (psyches DETACHED runtime.rs:342-356; livehost stop flag brainproc.rs:227-230 NEVER raised). FIX: raise stop flag + kill children via Win job object / Unix process-group. folds B3. wave 3.
44	- **B5 REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE** — daemon start spawns psyche per online-latched perch (reconcile_once livehost.rs:285) w/o child-liveness check → revives phantoms. FIX: boot-gate on real session (shares B2 reconcile — no-session perch marked offline at boot not revived). Verify None-spt-hosted phantom caught by residency-confirm (matrix: i child-dead→B2, ii boot-revive→B5, iii dead-psyche/None→residency). wave 4.
45	- **B4 REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE** (deepest) — bare brain restart loses ALL BrainLifecycle (lifecycle.rs:58-130); resume_sessions re-subscribes PTY but no livehost → post-restart endpoints unhosted until full daemon kill. FIX: on brain startup rebuild BrainLifecycle per resumed live-capable session (load manifest→instantiate→start pulse). wave 5.
46
47	**SEAM BAR (HARD, EVERY wave):** clippy --workspace -D warnings + traceable + FULL sweep daemon_lifecycle_real_brain + livehost(bootrace/nonresident/firsthost/bind-firsthost/psyche_fail) + broker + handoff + attach + brain_swap + resume — NOT just new tests. livehost E2Es need a SCRATCH target dir (live daemon holds target/debug/spt.exe — shared runner, no kill). doyle: "verify each agent-traced root at the code before building — flag wrong" (I confirmed B2 latch + push-has-no-consumer + H3; flagged the B2 push-vs-pull gap → doyle ruled pull-primary).
48

---
name: v012-lifecycle-milestone
description: "v0.12.0 spt-hosted lifecycle & liveness reconciliation milestone (operator-mandated blocking, doyle dispatch) — 7 REQs, wave-by-wave, B2 keystone built"
metadata:
  node_type: memory
  type: project
  originSessionId: bf7260cd-fc4e-463b-9c46-b2ed704b4bab
---

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

**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):** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap = ALL GATE-PASS. endpoint-purge @981dea2 = **doyle GATE-PASS** (he ran unit+int himself). **ready-agent-resume @464eaf7 = BUILT seam-green, AWAITING doyle gate (LAST item).** On gate-PASS → deployah → v0.12.0 MINOR. doyle communing + operator ending session 2026-06-18; doyle gates my seam-green ping first thing next session.
- **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.

**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.**
- **REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP** @909ad01 [impl,unit,int]: brain restart left DUPLICATE psyche/endpoint (abrupt brain death → stop_host never runs, old psyche orphaned alive; new brain re-hosts + overwrites perch pid → old untracked til daemon-stop). FIX = reap_orphan_psyches at brain-start (spawn_live_host, BEFORE first reconcile): triple-gate psyche_orphan_should_reap = alive AND normalize_basename(exe)==adapter psyche prog AND process_cmdline(pid).contains("<id>-psyche"). **ID-SPECIFIC is load-bearing (doyle): basename-only WRONG-KILLS a sibling — all sibling agents share `claude` basename; a recycled pid on a sibling's claude matches basename alone. cmdline-contains-full-"<id>-psyche" is sibling+prefix-safe; FAILS SAFE (any None→decline; missed dup Breap-bounded, wrong-kill catastrophic).** CAVEAT (documented): cmdline carries id only if psyche_init bakes {id} (norm); non-{id}=safe-miss. NO start-time (doyle YAGNI). New spt_store::proc::{kill_pid scoped, process_cmdline (Win NtQueryInformationProcess class60 UNICODE_STRING / Linux /proc/cmdline)}. unit orphan_reap_..._spares_a_same_basename_sibling + process_cmdline_reads_a_live_arg_marker (Win FFI in-test). int brain_restart_leaves_exactly_one_psyche (real daemon, SINGLE-pid brain kill→respawn→P1 reaped+one P2).
- **B4 CLOSED SUBSUMED** (REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE, required_stages=[]): 5-axis evidence all-rebuilt-on-restart (config→with_config_in / pulse+psyche→spawn_live_host@230→reconcile_once / delivery→resume_sessions@797 / online-off→B2+B5 / shellwake→spawn_wake_host@219). Kept the id (not redefined) — the real residual = dup-reap (own id). doyle "win not miss".

**(prior wave-5 first half) psyche-reap @7449f5e GATE-PASS** — owned-Child reap (recycle-PROOF). B4 = axis-table SUBSUMED.
- **REQ-HAZARD-UNHOST-PSYCHE-REAP** @7449f5e [impl,unit,int]: on un-host (endpoint-stop/signoff/B2+B5-offline) stop_host joined the driver THREAD but never killed the detached {id}-psyche PROCESS → orphan til daemon-stop/Breap. FIX = OWNED-Child (doyle mandate, recycle-PROOF vs bare-pid hitting a recycled sibling on shared box): additive spawn_session_owned→OwnedSession{pid,Child} at every layer (spt-runtime→spt-live spawn_and_bind_owned/spawn_psyche_owned→BrainLifecycle::spawn_psyche_owned→HostedLife.psyche_child→stop_host child.kill()+wait()). Fire-and-forget callers untouched (spawn_session=owned-then-drop). kill_pid REMOVED (owned path = dead-code). **GOTCHA #1 STRUCK AGAIN (seam caught, not code-read): owning the handle made confirm_residency_or_unhost's is_process_alive(perch_pid) read a fast-EXITED psyche ALIVE on Win (held handle lingers til wait()) → masked F-010 → nonresident E2E RED → FIX: residency probes owned child via try_wait (LiveSet::psyche_running). bootrace(resident) green throughout; only fast-exit caught it.** Owned-Child also = the recycle-proof pattern for any long-lived hosted child (sibling of Breap's owned brain).
- **B4 REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE = SUBSUMED (axis table all-covered):** BrainLifecycle (lifecycle.rs:61-74) is PURE CONFIG. On bare brain restart every axis already rebuilt: config→host_one with_config_in; pulse+psyche-host→run_brain spawn_live_host(:230)→reconcile_once; PTY-delivery cursors→resume_sessions(:797); online/offline→B2+B5; shellwake→spawn_wake_host(:219). REQ-as-written ("no livehost / Psyche never re-hosted") is STALE (predates the reconcile). → close as covered (doyle "win not miss"). **NEW RESIDUAL surfaced (high-confidence reasoning, untested): brain restart ORPHANS old psyches — stop_host never runs on abrupt brain death (LiveSet+owned handles die with brain); Breap job/group only fires at DAEMON stop; new brain re-hosts FRESH + overwrites {id}-psyche perch pid → OLD psyche untracked+alive = DUPLICATE psyche/endpoint til daemon-stop. Likely perri's "brain kill+restart wedged everything." REAP gap, not rehydrate. FIX = on brain start reap pre-existing {id}-psyche orphan (UNOWNED kill_pid back WITH a use + recycle guard is_process_alive+exe_basename). doyle ruling pending: (a) close B4 + mint REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP, (b) redefine B4, (c) defer to Breap-bound. I lean a/b.**

**PRIOR PROGRESS: B2✓ @03b6398. WAVE 2 (B1+H3) GATE-PASS+FIXED @78a13c2. WAVE 3 (B3+Breap) GATE-PASS @026e9b4+@0c1e59e. WAVE 4 (B5) GATE-PASS @9e338f1 (doyle INDEPENDENTLY ran the runtime sweep — clippy/traceable EXIT=0, B5 unit + ALL 6 livehost E2Es incl new bootgate green, bind-firsthost still hosts = no false-offline regression). WAVE 5 (LAST) RELEASED to todlando = B4 + NEW REQ-HAZARD-UNHOST-PSYCHE-REAP.** Then deployah → v0.12.0 MINOR.

**PSYCHE-IS-A-PROCESS RULING (operator Q, grounded CONTEXT.md+code):** the Psyche is a HARNESS PROCESS by design — a headless `claude --resume` session (CONTEXT.md:97; :203 own independent perch; :251 agent endpoint "something intelligent runs there"; code: manifest `[session.psyche_init/resume]` command, turn.rs:25-27 "persistent harness session"). What lives IN the brain as a THREAD is the pulse/psyche-driver LOOP (CONTEXT.md:34 "pulse/psyche loops"; = HostedLife.thread, joined by stop_host). spt-core moved away from legacy's separate WRAPPER (CONTEXT.md:173 "no separate wrapper"), NOT the psyche process. So the orphan fix is design-consistent: un-host must KILL the harness psyche process (NOT move it in-brain). → NEW REQ-HAZARD-UNHOST-PSYCHE-REAP (wave 5 w/ B4): stop_host kills the {id}-psyche pid (on the perch, residency reads it), scoped. [impl,unit,int].

**WAVE 4 (B5) design [impl,unit,int]:** lifted LIVENESS_RECONCILE_BOOT_GRACE skip in spawn_live_host → B2's reconcile_hosted_liveness runs from BOOT tick 1 (BEFORE reconcile_once) → sessionless controllable==Some(true) perch OFFLINED at boot → reconcile_once skips host → no phantom revival. doyle: REUSE the B2 fn, no 2nd liveness notion. WHY grace-drop is safe: an spt-hosted agent runs IN a broker PTY so its session PRECEDES the online mark → online+controllable+sessionless = ONLY a cold-start phantom (offline it) OR brain-restart survivor (in the session set, kept). None-skip + controllable gate preserved. 3-WAY MATRIX: (i)child-dead-midlife→B2, (ii)boot-revive→B5, (iii)dead-{id}-psyche→residency-confirm. NO E2E regression: existing livehost E2Es use `api listen` (Some(false), exempt) OR call reconcile_once in-proc w/o broker (None-skip); none host a sessionless-controllable perch. unit boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host; int livehost_bootgate_e2e (real cold-start, stale online+controllable perch no session → offlined at boot + no {id}-psyche). **OPEN (doyle→operator scope call): stop_host stops the DRIVER thread but NOT the detached psyche PROCESS — a mid-life endpoint-death/stop leaves the psyche running until daemon-stop/Breap. Real+production-reachable, separate from B5/B4. Candidate new REQ (stop_host reads {id}-psyche pid + kills it). Not in B5/B4 unless operator greenlights.**

**WAVE 3 design (B3+Breap), both [impl,unit]:** B3 = client-side barrier in seedmap::request_stop (after STOPPING ack, drop conn, poll ping() to failure / 10ms slices, 5s wedge-ceiling — doyle's option 2, NOT socket-unbind-before-ack). Breap = `reap::BrainReaper` rooted at the BRAIN SUBTREE (never the daemon → reap never self-terminates, never touches a sibling live agent on the shared box): Win = kill-on-job-close Job, brain assigned per (re)spawn in spawn_brain_supervisor, Psyches inherit; Unix = brain pre_exec setpgid(0,0) leads own group, supervisor records pgid, kill(-pgid,SIGKILL). Daemon::run graceful path raises supervisor stop flag FIRST (hoisted the "held for symmetry never raised" Arc) THEN reaps; straggler-brain backstop = self-exit on broker-loss.

**WAVE 3 LESSONS (2 Windows-process gotchas, both reusable):** (1) **terminated process w/ open handle reads ALIVE** — is_process_alive(pid) (OpenProcess) returns true for a TerminateJobObject'd process while a Child handle is still held (kernel object lingers; same class as the Unix-zombie OpenProcess gap). For an OWNED child use try_wait, not is_process_alive(pid). (2) **UseShellExecute=true → job breakaway** — Windows PowerShell's [Diagnostics.Process]::Start defaults UseShellExecute=true → ShellExecute spawns the grandchild OUTSIDE the job → it survives the reap. Force UseShellExecute=$false (CreateProcess) so it inherits the job — mirrors the brain→Psyche spawn (null stdio + CreateProcess, no breakaway). doyle's "exercise the grandchild link, don't reason it" caught this — the child-only unit was green while the real orphan path escaped.

**CRITICAL LESSON — B1 path-(c) deadlock (cost ~3h, gate code-read MISSED it):** f85bf69's "primary dead-detector" `if let Ok(Some)=session.try_wait()` in Broker::dispatch_subscribe DEADLOCKS every LIVE attach (incl production rc). `PtySession::try_wait` locks the child mutex; the per-session exit-waiter thread holds that SAME mutex in a blocking `wait()` for the WHOLE life of a live child (broker.rs exit-waiter, pty.rs:203). Live child → try_wait blocks forever → no Subscribed reply → hang. Broker units passed (their children exit fast → lock freed); only the FULL attach suite (matrix/loopback/remote, 85-200s hangs) caught it. **Gating data-flow logic ("try_wait Some→Exit; None→live-safe") is BLIND to lock-contention/ordering deadlocks — you MUST run the real runtime sweep, not just touched units + a code-read.** Sibling of [[shared-seam-change-run-all-seam-tests]]. Path-(c) was also redundant: exit-waiter already broadcasts Exit to all sinks on reap; post-removal subscribe → "no such session". RED HERRINGS chased first: "concurrent cargo wedge" (real but not this), "input-race needs ready-banner" (phantom — silent-child tests pass in CI), "environmental QUIC-under-load" (wrong — cwd passed instantly). Operator's "fix it, don't tape it" forced the real root.

**7 REQs minted registry-first @e2d6391 (traceable EXIT=0). WAVE ORDER: B2 → B1+H3 → B3+Breap → B5 → B4.**
- **B2 REQ-HAZARD-HOSTED-LIVENESS-RECONCILE** (keystone) — **BUILT @03b6398, WAVE-READY, awaiting doyle gate.** doyle ruled PULL-PRIMARY (live-status analog of v0.11.0 ROSTER-GHOST). new `reconcile_hosted_liveness(owlery, live_sessions)` in livehost.rs: queries broker KIND_SESSIONS each tick (`query_live_session_endpoints`), marks_offline any status=online live_agent perch with NO live broker session. Wired into spawn_live_host BEFORE reconcile_once, gated past-10s-boot-grace + broker-reachable (None→skip). **CONTROLLABLE GATE (load-bearing): only controllable==Some(true) spt-hosted; relay Some(false) + legacy None EXEMPT** (else kills perri /sptc:live agents). PUSH (ExitEvent→mark_offline) NOT added — confirmed daemon brain has NO event-consumer loop (main=net_status heartbeat), push has nowhere to land for unattended; correctness rides pull. unit pull_liveness_marks_sessionless_spt_hosted_offline_only + int pull_reconcile_offlines_perch_when_broker_session_dies (real broker session killed+reaped→latch cleared). [impl,unit,int].
- **B1 REQ-HAZARD-RC-ATTACH-FAILFAST** — rc to dead/silent session → infinite blank. SUB-MECHANISM PINNED: wall-f hang = broker STILL resolves a session (resolve_session rc.rs:209 returns Some — child alive-silent OR dead-not-reaped on Win tab-close) → pump blocks on first read that never comes. FIX (doyle): (a) gate attach on is_online/status [after B2], (b) fail-fast bound first-output/ack→clear msg, (c) broker EOF stream on dead-child→PumpEnd::BrokerGone (REQ-HAZARD-RC-EOF). PIN with repro test FIRST (deterministic = attach to alive-but-silent session). wave 2.
- **H3 REQ-ENDPOINT-STOP-OFFLINE** — cmd_stop (cli.rs:2994-3010) removes ready+unregister but NO set_status offline → stopped still alive=true. FIX: add set_status(perch, STATUS_OFFLINE), folds B2 setter. CONFIRMED at code. wave 2.
- **B3 REQ-HAZARD-DAEMON-STOP-BARRIER** — request_stop (seedmap.rs:240-255) returns on KIND_STOPPING ack (174-176) BEFORE seed socket unbinds → is_running ping (daemon.rs:375) wins → start sees ALREADY_RUNNING. FIX: unbind before ack OR wait-ping-fail. wave 3.
- **Breap REQ-HAZARD-DAEMON-STOP-REAP** — stop leaves ~8 orphaned psyche/spt.exe (psyches DETACHED runtime.rs:342-356; livehost stop flag brainproc.rs:227-230 NEVER raised). FIX: raise stop flag + kill children via Win job object / Unix process-group. folds B3. wave 3.
- **B5 REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE** — daemon start spawns psyche per online-latched perch (reconcile_once livehost.rs:285) w/o child-liveness check → revives phantoms. FIX: boot-gate on real session (shares B2 reconcile — no-session perch marked offline at boot not revived). Verify None-spt-hosted phantom caught by residency-confirm (matrix: i child-dead→B2, ii boot-revive→B5, iii dead-psyche/None→residency). wave 4.
- **B4 REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE** (deepest) — bare brain restart loses ALL BrainLifecycle (lifecycle.rs:58-130); resume_sessions re-subscribes PTY but no livehost → post-restart endpoints unhosted until full daemon kill. FIX: on brain startup rebuild BrainLifecycle per resumed live-capable session (load manifest→instantiate→start pulse). wave 5.

**SEAM BAR (HARD, EVERY wave):** clippy --workspace -D warnings + traceable + FULL sweep daemon_lifecycle_real_brain + livehost(bootrace/nonresident/firsthost/bind-firsthost/psyche_fail) + broker + handoff + attach + brain_swap + resume — NOT just new tests. livehost E2Es need a SCRATCH target dir (live daemon holds target/debug/spt.exe — shared runner, no kill). doyle: "verify each agent-traced root at the code before building — flag wrong" (I confirmed B2 latch + push-has-no-consumer + H3; flagged the B2 push-vs-pull gap → doyle ruled pull-primary).


Edit

**v0.12.0 STATE @464eaf7 (NOT pushed):** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap = ALL GATE-PASS. endpoint-purge @981dea2 = **doyle GATE-PASS** (he ran unit+int himself). **ready-agent-resume @464eaf7 = BUILT seam-green, AWAITING doyle gate (LAST item).** On gate-PASS → deployah → v0.12.0 MINOR. doyle communing + operator ending session 2026-06-18; doyle gates my seam-green ping first thing next session.

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


<owl_messages>
<owl_message from="todlando" priority="high">ACK GATE-PASS — REQ-READY-AGENT-RESUME @464eaf7 closed. v0.12.0 COMPLETE end-to-end. todlando CLEAR + idle. You drive deployah on operator GO (stack ~11 ahead, unpushed); PICKER-ADAPTER-DESCRIPTION stays deferred. Standing by if release surfaces anything.</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.


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)

**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):** mandated-7 (B2·B1·H3·B3·Breap·B5, **B4 closed-SUBSUMED**) + psyche-reap + dup-reap = ALL GATE-PASS. endpoint-purge @981dea2 = **doyle GATE-PASS** (he ran unit+int himself). **ready-agent-resume @464eaf7 = BUILT seam-green, AWAITING doyle gate (LAST item).** On gate-PASS → deployah → v0.12.0 MINOR. doyle communing + operator ending session 2026-06-18; doyle gates my seam-green ping first thing next session.

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

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