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();
        }
    }
}


Ideal pattern: `live_bind_firsthost_e2e` uses in-process `reconcile_once` (reliable, no PTY). I'll mirror it inverted (no-psyche_init → no host). But first verify: does a harness-hosted ready perch carry `adapter` stamped? `start_homed` doesn't set it — that decides whether reconcile reaches the psyche_init gate vs skips at the adapter gate. Check where `adapter` is stamped on endpoint perches.


Bash

grep -rn "\.adapter = \|set_adapter\|adapter: Some\|rec.adapter\|info.adapter" crates/spt/src/ crates/spt-daemon/src/ crates/spt-msg/src/ | grep -iv test | head -30

Find where endpoint perch adapter is stamped


crates/spt/src/api/reporting.rs:351:    let shell = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
crates/spt/src/api/reporting.rs:358:            info.adapter_name
crates/spt/src/api/reporting.rs:970:            adapter: Some("m".into()),
crates/spt/src/api/reporting.rs:977:            adapter: Some("m".into()),
crates/spt/src/api/startup.rs:684:            rec.adapter.as_deref(),
crates/spt/src/cli.rs:1431:    rec.adapter = src_rec.adapter.clone();
crates/spt/src/cli.rs:5935:        let shell = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
crates/spt/src/cli.rs:5942:                id, info.adapter_name
crates/spt/src/cli.rs:6240:                let shell = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
crates/spt/src/cli.rs:9624:        rec.adapter = Some("mock".into());
crates/spt/src/cli.rs:9639:        assert_eq!(new_rec.adapter.as_deref(), Some("mock"), "adapter carries");
crates/spt-daemon/src/digest.rs:528:            rec.adapter = Some("ccspan".to_string());
crates/spt-daemon/src/linkhost.rs:201:    let manifest = spt_runtime::registry::resolve_option(&adapters_dir, &info.adapter_name)
crates/spt-daemon/src/linkhost.rs:206:        return Err(ShellLinkError::AdapterUnregistered(info.adapter_name));
crates/spt-daemon/src/linkhost.rs:419:    let pid = shellhost::launch_shell(owlery, owner, &id, &info.adapter_name, &shell)
crates/spt-daemon/src/livehost.rs:325:        let Some(adapter) = info.adapter.as_deref() else {
crates/spt-daemon/src/livehost.rs:622:        let Some(adapter) = info.adapter.as_deref() else {
crates/spt-daemon/src/livehost.rs:780:        rec.adapter = Some(adapter.to_string());
crates/spt-daemon/src/livehost.rs:880:                rec.adapter = Some("mock".to_string());
crates/spt-daemon/src/shelldisc.rs:136:            Some(a) => format!("{a} → {id} ({}), {}", info.adapter_name, info.status),
crates/spt-daemon/src/shelldisc.rs:137:            None => format!("{id} ({}), {}", info.adapter_name, info.status),
crates/spt-daemon/src/shellhost.rs:512:        let shell = shell_section_of(registered, adapters_dir, &info.adapter_name);
crates/spt-daemon/src/shellhost.rs:519:                if let Err(e) = launch_shell(owlery, owner, &shell_id, &info.adapter_name, shell) {
crates/spt-daemon/src/shellwake.rs:486:            let Some(shell) = shell_section_of(registered, adapters_dir, &info.adapter_name) else {
crates/spt-daemon/src/shellwake.rs:498:            let tokens = match fill_wake_command(&shell_id, &info.adapter_name, wake_command) {
crates/spt-daemon/src/shellwake.rs:511:                let adapter_name = info.adapter_name.clone();
crates/spt-daemon/src/shellwake.rs:958:        info.adapter_name = "mock-wakeprof:waker".to_string();


livehost.rs:780 stamps `rec.adapter` — likely the daemon stamping it. Check that context.


760	    fn register_live_adapter(name: &str, drops: &Path) {
761	        let dir = drops.to_string_lossy().replace('\\', "/");
762	        let toml = format!(
763	            "[adapter]\nname=\"{name}\"\nkind=\"harness\"\nversion=\"1\"\n\
764	             min_spt_core_version=\"0\"\n\n\
765	             [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
766	             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
767	        );
768	        let src = perch::spt_home().join("srcs").join(name);
769	        std::fs::create_dir_all(&src).unwrap();
770	        std::fs::write(src.join("manifest.toml"), toml).unwrap();
771	        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
772	    }
773
774	    /// Seed a Self **live** perch on disk: `state=live_agent`, the given status,
775	    /// and the adapter option that resolves the manifest.
776	    fn seed_live_perch(id: &str, adapter: &str, status: &str) {
777	        let perch = perch::resolve_perch_path(id, ParentHint::Infer);
778	        std::fs::create_dir_all(&perch).unwrap();
779	        let mut rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-1", "live_agent");
780	        rec.adapter = Some(adapter.to_string());
781	        spt_store::info::write_info(&perch, &rec).unwrap();
782	        spt_store::info::set_status(&perch, status).unwrap();
783	    }
784
785	    /// A fast-pulse config so the driver loop ticks quickly in tests.
786	    fn fast_cfg() -> DaemonConfig {
787	        DaemonConfig {
788	            pulse_period: Duration::from_millis(15),
789	            ..DaemonConfig::default()
790	        }
791	    }
792
793	    fn registered_now() -> Vec<(AdapterRecord, Manifest)> {
794	        spt_runtime::registry::registered(&perch::adapters_dir())

    fn register_live_adapter(name: &str, drops: &Path) {
        let dir = drops.to_string_lossy().replace('\\', "/");
        let toml = format!(
            "[adapter]\nname=\"{name}\"\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_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
        );
        let src = perch::spt_home().join("srcs").join(name);
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("manifest.toml"), toml).unwrap();
        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
    }

    /// Seed a Self **live** perch on disk: `state=live_agent`, the given status,
    /// and the adapter option that resolves the manifest.
    fn seed_live_perch(id: &str, adapter: &str, status: &str) {
        let perch = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&perch).unwrap();
        let mut rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-1", "live_agent");
        rec.adapter = Some(adapter.to_string());
        spt_store::info::write_info(&perch, &rec).unwrap();
        spt_store::info::set_status(&perch, status).unwrap();
    }

    /// A fast-pulse config so the driver loop ticks quickly in tests.
    fn fast_cfg() -> DaemonConfig {
        DaemonConfig {
            pulse_period: Duration::from_millis(15),
            ..DaemonConfig::default()
        }
    }

    fn registered_now() -> Vec<(AdapterRecord, Manifest)> {
        spt_runtime::registry::registered(&perch::adapters_dir())


That's a test helper hand-stamping adapter — legitimizes my approach. Check the reconcile loop's status/type gate before the adapter gate (do I need to stamp status online?).


300	        confirm_residency_or_unhost(set, &id);
301	    }
302
303	    // Start side: every online live endpoint not yet hosted.
304	    for id in perch::list_self_perch_ids(owlery) {
305	        if set.contains(&id) {
306	            continue; // already hosted (leave-alone)
307	        }
308	        // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT] a just-failed residency endpoint is
309	        // held off until its cooldown expires — do NOT re-spawn the doomed psyche this
310	        // same sweep (that would clear+recreate the phantom + churn). One quiescent
311	        // window, then a single retry the tick after the cooldown.
312	        if set.in_nonresident_cooldown(&id) {
313	            continue;
314	        }
315	        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
316	        let Some(info) = spt_store::info::read_info(&perch) else {
317	            continue;
318	        };
319	        if info.state != LIVE_AGENT_STATE {
320	            continue; // not a live agent (ready_agent etc.)
321	        }
322	        if info.status.as_deref() != Some(STATUS_ONLINE) {
323	            continue; // interim (status-absent) or signed-off (offline)
324	        }

        confirm_residency_or_unhost(set, &id);
    }

    // Start side: every online live endpoint not yet hosted.
    for id in perch::list_self_perch_ids(owlery) {
        if set.contains(&id) {
            continue; // already hosted (leave-alone)
        }
        // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT] a just-failed residency endpoint is
        // held off until its cooldown expires — do NOT re-spawn the doomed psyche this
        // same sweep (that would clear+recreate the phantom + churn). One quiescent
        // window, then a single retry the tick after the cooldown.
        if set.in_nonresident_cooldown(&id) {
            continue;
        }
        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
        let Some(info) = spt_store::info::read_info(&perch) else {
            continue;
        };
        if info.state != LIVE_AGENT_STATE {
            continue; // not a live agent (ready_agent etc.)
        }
        if info.status.as_deref() != Some(STATUS_ONLINE) {
            continue; // interim (status-absent) or signed-off (offline)
        }


Critical correction: reconcile **START side** skips at the STATE gate first — livehost.rs:319 `if info.state != LIVE_AGENT_STATE { continue; // ready_agent etc. }` — BEFORE the adapter(325)/psyche_init(343) gates. So a ready endpoint (state=ready_agent, what `start_homed` writes) is never hosted regardless of manifest. The psyche_init gate is the secondary discriminator for a live-STATE perch. Still SUBSUMED — more directly. Correct doyle + write the int faithfully (no synthetic live-state stamping).


$OWL send doyle <<'EOF'
FIX2 verdict correction (strengthens SUBSUMED, doesn't weaken): for a READY endpoint the precise no-psyche gate is the reconcile START-side STATE gate — livehost.rs:319 `if info.state != LIVE_AGENT_STATE { continue; // ready_agent etc. }` — which fires BEFORE the adapter(325) + psyche_init(343) gates. A ready bind (start_homed) writes state="ready_agent", so reconcile skips it at the state gate → no psyche, regardless of manifest. The psyche_init→with_config_in→None gate I cited is the SECONDARY discriminator for a LIVE-state perch w/ a non-psyche manifest. Net: still zero code, even more directly subsumed (state-typed, exactly operator's "type = manifest/endpoint concern"). Int asserts: real `spt ready` bind ledgers Boot + reconcile_once hosts NO {id}-psyche (state gate). Writing int now.
EOF

Send FIX2 gate correction to doyle


➜ SENT:doyle


Strongest faithful design: adversarial — register a **live-capable** adapter (WITH psyche_init NOOP), stamp it on the real `spt ready` (state=ready_agent) perch, reconcile → assert NO psyche. Proves the STATE gate isolates type from manifest (direct inversion of `live_bind_firsthost`). Confirm `common` helpers + write the int.


1	//! Shared test helpers for the `spt` integration tests.
2	//!
3	//! `tests/common/mod.rs` (a subdirectory module) is NOT compiled as its own test
4	//! binary — each test file pulls it in with `mod common;`.
5
6	use std::process::Command;
7
8	/// Chainable: suppress the ephemeral console window a Windows console-subsystem
9	/// child pops when the test harness has a console (every `spt daemon`/`api`/
10	/// `brain` subprocess a test spawns). No-op on non-Windows. CREATE_NO_WINDOW does
11	/// NOT detach — env/stdio inheritance is unchanged — it only hides the console.
12	///
13	/// Usage: `Command::new(bin).no_window().args([...]).spawn()`.
14	pub trait CommandNoWindowExt {
15	    fn no_window(&mut self) -> &mut Command;
16	}
17
18	impl CommandNoWindowExt for Command {
19	    #[cfg(windows)]
20	    fn no_window(&mut self) -> &mut Command {
21	        use std::os::windows::process::CommandExt;
22	        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
23	        self.creation_flags(CREATE_NO_WINDOW)
24	    }
25
26	    #[cfg(not(windows))]
27	    fn no_window(&mut self) -> &mut Command {
28	        self
29	    }
30	}
31

//! Shared test helpers for the `spt` integration tests.
//!
//! `tests/common/mod.rs` (a subdirectory module) is NOT compiled as its own test
//! binary — each test file pulls it in with `mod common;`.

use std::process::Command;

/// Chainable: suppress the ephemeral console window a Windows console-subsystem
/// child pops when the test harness has a console (every `spt daemon`/`api`/
/// `brain` subprocess a test spawns). No-op on non-Windows. CREATE_NO_WINDOW does
/// NOT detach — env/stdio inheritance is unchanged — it only hides the console.
///
/// Usage: `Command::new(bin).no_window().args([...]).spawn()`.
pub trait CommandNoWindowExt {
    fn no_window(&mut self) -> &mut Command;
}

impl CommandNoWindowExt for Command {
    #[cfg(windows)]
    fn no_window(&mut self) -> &mut Command {
        use std::os::windows::process::CommandExt;
        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
        self.creation_flags(CREATE_NO_WINDOW)
    }

    #[cfg(not(windows))]
    fn no_window(&mut self) -> &mut Command {
        self
    }
}


Write

//! 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)"
    );
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt\tests\ready_resume_ledger_e2e.rs (file state is current in your context — no need to Read it back)

//! 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)"
    );
}


Int written. Now CONTEXT.md clarification (keep CONTEXT:180 ready path). Read around there + where `endpoint run` is described.


Bash

grep -n "endpoint run\|harness-hosted\|ready agent\|ready_agent\|psyche_init\|Resume from history\|resume" docs/CONTEXT.md | head -30

Locate CONTEXT.md endpoint-run/ready sections


grep: docs/CONTEXT.md: No such file or directory


find . -name "CONTEXT.md" -not -path "*/target*/*" 2>/dev/null; echo "==="; ls *.md docs/*.md 2>/dev/null | head

Find CONTEXT.md location


./CONTEXT.md
===
AGENTS.md
CHANGELOG.md
CI-SELFHOST-PLAN.md
CLAUDE.md
CONTEXT.md
DAEMON-LIFECYCLE-COMPLETION-PLAN.md
DAEMON-LIFECYCLE-PLAN.md
DAEMON-LIFECYCLE-VERIFICATION.md
DIGEST-MILESTONE-PLAN.md
M0-PLAN.md


grep -n "endpoint run\|harness-hosted\|ready agent\|ready_agent\|psyche_init\|Resume from history\|resume\|start_homed" CONTEXT.md | head -40

Locate endpoint-run/ready sections in CONTEXT.md


39:A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no relay — the daemon injects events straight into the PTY.
61:A declarative configuration file (TOML/YAML — schema TBD) that tells spt-core how to drive a specific harness. Declares: how to invoke an agent session, how to look up conversation history for an echo commune, how to spawn/resume a Psyche-equivalent, which binary or command implements each harness-side operation, and which endpoint types this harness supports. spt-core is the actor for each of these; the manifest tells it what to do.
97:How the harness spawns agent sessions, including Psyche and echo-commune sessions. For the rebuilt spt-plugin, Psyche and echo communes must migrate **off `claude -p`** (imminent Claude Code billing changes) to headless `claude` sessions (`--resume` for the Psyche). This is an adapter/manifest concern, not a core concern, but the parity milestone must carry it.
103:**spawn-session seam** — launch a new agent session on this node. Manifest provides: a command template; `cwd`/project; a `headless` flag (optional, default false — for the GUI's resume-of-compatible-adapters); a `resume` flag (optional); and the `commune` + `signoff` file directories relative to `cwd` (so the daemon knows where to watch). Substitution keys spt-core can supply: `{id}` and, optionally, a spt-core-generated valid session UUID (e.g. injected as `--session-id {uuid}`) so an adapter can skip the post-spawn seam. spt-core does **not** inject: harness-internal env (broker handles binary env; adapter handles in-session env), and **no initial-context handoff** (not needed at start — the agent is prompted for context once its session is up; the first commune populates it).
110:**spawn-psyche seam** — two command templates: fresh-start and resume (the resume template includes `$session_id`). Both include `$psyche_prompt` — the revival essentials spt-core feeds the Psyche (timestamp, incoming event envelope). Everything else is the adapter's: model selection (in its template), and any harness-specific instructions the Psyche needs (Write-tool usage, commune dir) supplied as a static preamble before `$psyche_prompt` or as adapter SessionStart additionalContext. spt-core owns `$psyche_prompt` content; the adapter owns the rest.
112:**history subsystem** (covers echo-commune source logs, resume briefs, and Shell logs) — two supported paths:
127:**resume-session seam** — two distinct forms:
128:- **fresh-with-preload:** resume with *cleared* context (a fresh session) + psyche-download. Accepts a `$psyche-context` key to launch the fresh session with the psyche-download preloaded — or the adapter instead pulls it via an spt-core command in its SessionStart hook.
129:- **continue-existing:** resume an existing harness session under the adapter (its native resume).
138:- **A command template's program token resolves against the adapter install dir before PATH (since v0.8.0).** A `.spt` adapter ships its built binaries to the adapter's install dir (`adapters/_github/<safe>/` via `--release`/`--github`, or the record's `source_dir` under copy-mode), so a bare program name (e.g. `claude-spt-digest …`) binds to the shipped binary first and falls back to PATH when absent — a `.spt` that ships its binaries is **self-contained**, needing no PATH placement. <!-- [doc->REQ-INSTALL-11] --> Applies to the `[digest]` extractor, the `[session.psyche_init]` runner, and the `adapter digest-proof` tool; the install dir is the registry record's `source_dir` (precise) for the daemon-resolved paths — the `[digest]` extractor and the daemon-hosted `[session.psyche_init]` runner, where the brain's live-host reconcile resolves the Psyche program against the matching record's `source_dir` — and the `--manifest` file's parent dir when an explicit `--manifest` overrides on the api seam.
140:- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.
153:- **`api listen`** — *long-running* relay/poll listener that an adapter-owned (harness-hosted) session owns as a child process; streams the daemon's events to the session's stdout. Distinct from the short-lived `api poll`. This is the heir to today's Monitor-bound `$LIVE start` poll loop.
158:- **`api boundary <clear|compact>`** — context-boundary report; **carries the new `session_id`** (it rotates on `/clear` or `/compact`), so the daemon rebinds the perch to the new session id while keeping the stable identity + `parent_pid` anchor. Authors a **Self-resume commune** (resume the Self session → commune file-drop) rather than a background echo — strong live-context signal at the boundary (see `docs/CONTEXT-MEMORY.md`).
180:   - The same seed + bind-time resolution serves a **ReadyAgent** bringup (`$SPT ready`/poll), not just a LiveAgent — a harness-hosted ready agent is seeded and resolved identically (it just binds a poll listener, no Psyche).
196:So the old `$LIVE start` splits by topology: harness-hosted = SessionStart writes an adapter-agnostic seed → `$SPT listen <id>` consumes seed (by `parent_pid`) + resolves adapter/profile (ADR-0021) + binds + relays — legacy parity (`$LIVE start <id>` → `$SPT listen <id>`, no mandatory `--adapter`); spt-hosted = daemon spawn-session + `api bind` (direct, no file). The asymmetry is the file-bridge-only-when-no-direct-channel principle.
205:Minimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's "ready agent".
343:**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)
345:**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):
347:The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no
349:non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>
361:only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes
363:through the one bringup core (`spt endpoint run` / the rc pump), never a second path. A single
372:only: adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches
408:A rolling, human-glanceable view of an endpoint's **recent context** — *what the agent did* (recent user turns + the agent output between them, with **tool-usage sprints collapsed**, e.g. `<endpoint_id> used: **Write** `dir/file.txt` · **Bash** `first ~25 cmd chars` · […]`) **and** *what spt fed the agent* (**context-injection** entries — session-start psyche download, echo-commune mirror, incoming owl messages — collapsed by default, expandable). One time-ordered, glanceable timeline. **How much** is shown (window depth, arg-truncation, sprint-collapse) is the adapter's declared preference, consumer-overridable — *not* a fixed spt-core count. Distinct from both the **raw scrollback ring** (unparsed bytes) and full **transcript history** (the conversation behind echo communes/resume): its job is *"what is this agent's recent context, at a glance,"* not conversation replay.
411:- **Topology-independent.** Because the source is logs, the digest works for harness-hosted endpoints too — Claude Code's per-session JSONL is Path A's worked example. Freshness = file-watch + adapter hook nudges; harnesses with no usable logs push entries directly via **`api digest-entry`** (the Path-B-style door). Near-realtime tailing needs an *incremental* normalize story (design-phase concern: Path A's normalize-command was shaped for occasional pulls).
498:The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected. **Identity is node-global, advertised per-subnet** (see *subnet membership*): one `ling` exists on a node (one mind, one `tracked/` context) and is advertised into each subnet the node belongs to, subject to per-subnet visibility (see *endpoint visibility*). The same bare name may exist as *distinct* endpoints in different subnets; the resolution policy forces qualification when both are visible. Within a single subnet a bare id is unique — enforced by a **join-time collision check**: when a node carrying endpoint `X` joins/advertises into a subnet that already has a different `X`, the clash is surfaced and resolved (rename one — below) before advertisement. An endpoint keeps an **ordered adapter history** (in `tracked/agents/<id>/meta.json`, synced) driving the resume UX: latest adapter is the default resume offer, then next-latest, … oldest, then "choose a different adapter". (Adapter-agnosticism is an *agent-endpoint* property; Shells are adapter/platform-bound — see Shells.)
501:At **creation**, the adapter is chosen from the node's **registered `kind="harness"` adapters whose `hostable_types` includes the endpoint's type** — **auto if exactly one qualifies, else chosen** (`--adapter` / picker): the same *auto-if-one-ask-if-many* rule as *home subnet*. The choice seeds the **head of the ordered adapter history** and the live `adapter_name`. **Changing** an endpoint's adapter is **not** a standalone operation — adapter is a *live-instance* property, so you change it by **launching/resuming the endpoint under a different registered adapter** (the resume UX surfaces the adapter history: head = default, then prior adapters, then "choose a different adapter"). For a currently-active instance this is a **resume-under-the-new-adapter** carrying context via the *fresh-with-preload* psyche-download (a session cycle, not a hot swap — the mind is adapter-agnostic). Same gate both times: the target adapter must be **registered on the node** and its `hostable_types` must include the endpoint's type.
513:- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).
525:**Commands** (user/agent-initiated lifecycle, under the `endpoint` noun since M8-D1): `spt endpoint suspend <id[@node]>` (suspend any of your instances from any node), `spt endpoint wake <id[@node]>` (explicitly wake a dormant/suspended instance), `spt endpoint shutdown` (an agent suspends its *own* endpoint — graceful, fires the suspend boundary signoff, cascades shells offline), `spt refresh` (spt-hosted only — an agent clears + resumes itself without stalling: `/clear` → commune capture → guaranteed post-clear resume signal so its turn restarts from immediate next-steps; see `docs/CONTEXT-MEMORY.md`).
539:- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.
733:**Removal (`spt adapter remove`) is soft-deregister:** the adapter is **hidden from new-creation / picker lists immediately**, but existing and live instances **keep running** under it (lazy — nothing is force-torn-down). An optional manifest **`uninstall` command template** (the inverse of the install/`[update]` avenue — e.g. `claude plugin uninstall spt`) cleans the adapter's own artifacts; it runs **once no instance is live** under the adapter (or immediately with `--force`, which cascades teardown). Once actually uninstalled, an endpoint whose synced **adapter history** still references it renders as **"needs install"** in the resume picker and offers `adapter add` (the `--github` path closes the loop).
753:This is the realization of the "guided resume" (resume by project or agent, XMB-style filtered selection) and "management GUI" (per-endpoint panes: latest session output, live/project context, psyche log) sketches. `attach` is one operation under this frontend, not the whole frontend.
755:**guided-resume picker (`spt resume`, no-arg)**:
756:The CLI sibling of the frontend's guided resume — one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project → on-node / other-project → off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance → attach/tap-in (no adapter step — already live under one); a **non-running** endpoint → into the **adapter selector** (*adapter selection*: history head = default → prior adapters → "choose a different adapter") → *home subnet* / other creation prompts as needed → launch; a **"+ new endpoint"** entry → the full creation flow. Off-node picks respect the reach + consent gates (remote-drive of your own running instance is ungated; a cold off-node launch is *instantiate-anywhere*, gated/deferred). It unifies the no-id `spawn-session` picker, the resume adapter-history UX, and locality resolution into one pipeline; the day-one frontend renders the same selection logic graphically.


CONTEXT.md is repo-root. Read the `spt endpoint run` picker section (345-372) to add the both-types clarification there.


345	**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):
346	<!-- [doc->REQ-RUN-PICKER] -->
347	The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no
348	`--adapter`/`--id`) opens an in-process **ratatui picker**; the **flagged** form is the
349	non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>
350	--start|--attach|--view`), untouched — a picker selection bakes exactly that path. **Layer 1**
351	picks the kind (*Create new* | *Pick existing*). **Create-new** chooses a registered
352	`kind="harness"` adapter with its shipped+local **profiles tree-nested**, then a
353	charset-validated id, then starts. **Pick-existing** selects a **category** (←→ over
354	`[<cwd-project> | Local node | Subnet]`), endpoints **grouped + alphabetically sorted** with a
355	**status square** (online green ■ / offline gray ▢ — the blue *attached* tri-state + *Kick* are
356	**W2.5**, a dedicated broker attach-presence slice), **type-to-filter** (`/`), a pinned keybind
357	legend, and a **two-pane** right-half description (harness `adapter:profile` · best-effort
358	project history newest→oldest · `endpoint description`). The **confirm** layer offers
359	status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally
360	(remote) · Change-harness-adapter (offline) · Fork · **Resume-from-history** (offline+LOCAL
361	only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes
362	that session id). **Invariant:** the picker is a pure front-end — every terminal action routes
363	through the one bringup core (`spt endpoint run` / the rc pump), never a second path. A single
364	action enum is the source of truth so a future tap-mode (phone PTY) layers on without
365	re-coupling to keybinds.
366	_Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.
367
368	**`spt-<id>` shortcut** (picker `s` keybind, M12-W2):
369	<!-- [doc->REQ-RUN-SHORTCUT] -->
370	From any pre-start options set, `s` writes (or updates) a **`<basename>-<id>` launcher** at the
371	project root that bakes the current selection's **non-interactive** flags (terminal actions
372	only: adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches
373	— Kick/Instantiate/Change-adapter/Fork — are not bakeable). The **basename is a parameter**:
374	harness-agnostic spt-core defaults to **`spt`** (→ `spt-<id>`, e.g. `spt-doyle`); an

**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):
<!-- [doc->REQ-RUN-PICKER] -->
The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no
`--adapter`/`--id`) opens an in-process **ratatui picker**; the **flagged** form is the
non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>
--start|--attach|--view`), untouched — a picker selection bakes exactly that path. **Layer 1**
picks the kind (*Create new* | *Pick existing*). **Create-new** chooses a registered
`kind="harness"` adapter with its shipped+local **profiles tree-nested**, then a
charset-validated id, then starts. **Pick-existing** selects a **category** (←→ over
`[<cwd-project> | Local node | Subnet]`), endpoints **grouped + alphabetically sorted** with a
**status square** (online green ■ / offline gray ▢ — the blue *attached* tri-state + *Kick* are
**W2.5**, a dedicated broker attach-presence slice), **type-to-filter** (`/`), a pinned keybind
legend, and a **two-pane** right-half description (harness `adapter:profile` · best-effort
project history newest→oldest · `endpoint description`). The **confirm** layer offers
status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally
(remote) · Change-harness-adapter (offline) · Fork · **Resume-from-history** (offline+LOCAL
only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes
that session id). **Invariant:** the picker is a pure front-end — every terminal action routes
through the one bringup core (`spt endpoint run` / the rc pump), never a second path. A single
action enum is the source of truth so a future tap-mode (phone PTY) layers on without
re-coupling to keybinds.
_Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.

**`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
project root that bakes the current selection's **non-interactive** flags (terminal actions
only: adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches
— Kick/Instantiate/Change-adapter/Fork — are not bakeable). The **basename is a parameter**:
harness-agnostic spt-core defaults to **`spt`** (→ `spt-<id>`, e.g. `spt-doyle`); an


Add the type-agnostic both-types clarification after the picker section. Grounded, keeps CONTEXT:180/205.


Edit

action enum is the source of truth so a future tap-mode (phone PTY) layers on without
re-coupling to keybinds.
_Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.

**`spt-<id>` shortcut** (picker `s` keybind, M12-W2):

action enum is the source of truth so a future tap-mode (phone PTY) layers on without
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):


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

action enum is the source of truth so a future tap-mode (phone PTY) layers on without
re-coupling to keybinds.
_Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.

**`spt-<id>` shortcut** (picker `s` keybind, M12-W2):

action enum is the source of truth so a future tap-mode (phone PTY) layers on without
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):

+**`spt endpoint run` is the spt-hosted bringup for BOTH endpoint types** (v0.12.0):

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

From any pre-start options set, `s` writes (or updates) a **`<basename>-<id>` launcher** at the


Activate REQ-READY-AGENT-RESUME stages (doc+impl+unit+int — I touched CONTEXT). Leave PICKER-ADAPTER-DESCRIPTION deferred.


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

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 = ["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)


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


611	required_stages = ["doc", "impl", "unit", "int"]  # doc by doyle (CONTEXT.md `spt endpoint purge`); built v0.12.0 wave 5. impl = cmd_endpoint_purge (offline-gate + --force stop-then-wait-offline; recursive remove_dir_all of the perch tree incl nested; unregister_address; ContextStore::remove_endpoint; access.json + visibility.json row removal) + EndpointCmd::Purge{id, yes, force}; unit = offline-gate refuses an online ep without --force + self-purge guard; int = create endpoint w/ perch + a-<id> context branch → purge → assert EVERY record gone (perch tree, registry row, a-<id> + p-* rows, access/visibility)
612	[[requirements]]
613	id = "REQ-READY-AGENT-RESUME"
614	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)"
615	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
616	[[requirements]]
617	id = "REQ-PICKER-ADAPTER-DESCRIPTION"
618	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)"
619	required_stages = []  # DEFERRED fast-follow (operator 2026-06-18) — NOT built in v0.12.0; registry-first placeholder
620	[[requirements]]

required_stages = ["doc", "impl", "unit", "int"]  # doc by doyle (CONTEXT.md `spt endpoint purge`); built v0.12.0 wave 5. impl = cmd_endpoint_purge (offline-gate + --force stop-then-wait-offline; recursive remove_dir_all of the perch tree incl nested; unregister_address; ContextStore::remove_endpoint; access.json + visibility.json row removal) + EndpointCmd::Purge{id, yes, force}; unit = offline-gate refuses an online ep without --force + self-purge guard; int = create endpoint w/ perch + a-<id> context branch → purge → assert EVERY record gone (perch tree, registry row, a-<id> + p-* rows, access/visibility)
[[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
[[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)"
required_stages = []  # DEFERRED fast-follow (operator 2026-06-18) — NOT built in v0.12.0; registry-first placeholder
[[requirements]]


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)


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

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)

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)"


All 4 stages tagged (doc/impl/unit/int). No `spt` subcommand added → no xtask gen / docs-drift needed. Now full seam bar, SERIAL, in target-seam. Start: clippy --workspace (exact CI invocation).


Bash

CARGO_TARGET_DIR=target-seam cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -30

Clippy workspace -D warnings (CI invocation) in target-seam


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


<task-notification>
<task-id>b7cl1c34t</task-id>
<tool-use-id>toolu_01RBUPSf9j8mZuzpEGF3zrMw</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\b7cl1c34t.output</output-file>
<status>completed</status>
<summary>Background command "Clippy workspace -D warnings (CI invocation) in target-seam" completed (exit code 0)</summary>
</task-notification>


Clippy compiling in background (won't run concurrent cargo). While it compiles, enumerate the test suites to run after — shared-seam lesson (start_homed/ledger/reconcile siblings).


echo "=== spt-msg tests touching ready/start_homed ==="; grep -rln "ReadyAgent::start\|start_homed\|run_listen\|sessions::" crates/spt-msg/src/ 2>/dev/null
echo "=== spt integration tests touching ready/reconcile/ledger/sessions ==="; grep -rln "ReadyAgent\|reconcile_once\|sessions::\|\"ready\"\|start_homed\|livehost::" crates/spt/tests/ crates/spt-daemon/tests/ 2>/dev/null
echo "=== spt-store sessions tests ==="; grep -rln "mod tests\|sessions" crates/spt-store/src/sessions.rs

Enumerate sibling test suites exercising the changed seams


=== spt-msg tests touching ready/start_homed ===
crates/spt-msg/src/ready.rs
crates/spt-msg/src/ring.rs
=== spt integration tests touching ready/reconcile/ledger/sessions ===
crates/spt/tests/contract_e2e.rs
crates/spt/tests/livehost_bootgate_e2e.rs
crates/spt/tests/live_bind_firsthost_e2e.rs
crates/spt/tests/live_firsthost_e2e.rs
crates/spt/tests/poll_envelope_e2e.rs
crates/spt/tests/quickstart_e2e.rs
crates/spt/tests/ready_resume_ledger_e2e.rs
crates/spt/tests/shell_sleepwake_e2e.rs
crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs
crates/spt-daemon/tests/two_origin_spanning.rs
=== spt-store sessions tests ===
crates/spt-store/src/sessions.rs


Prompt:
<task-notification>
<task-id>b7cl1c34t</task-id>
<tool-use-id>toolu_01RBUPSf9j8mZuzpEGF3zrMw</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\b7cl1c34t.output</output-file>
<status>completed</status>
<summary>Background command "Clippy workspace -D warnings (CI invocation) in target-seam" completed (exit code 0)</summary>
</task-notification>


Clippy exit 0 ✓. Now traceable check (separate, non-cargo), then fast unit/store tests serially in target-seam.


traceable-reqs check 2>&1 | tail -25; echo "TRACEABLE_EXIT=${PIPESTATUS[0]}"

traceable-reqs check (expect EXIT=0)


       Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL.
  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
  [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).
TRACEABLE_EXIT=0


traceable EXIT=0 ✓. Now tests serially in target-seam. Fast first: spt-msg lib (new unit + ready siblings) + spt-store sessions seam.


CARGO_TARGET_DIR=target-seam cargo test -p spt-msg --lib 2>&1 | tail -20 && echo "===SPT-STORE SESSIONS===" && CARGO_TARGET_DIR=target-seam cargo test -p spt-store --lib sessions 2>&1 | tail -15

spt-msg lib tests + spt-store sessions tests (serial)


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


until [ -f /dev/null ] && grep -qE "test result:|error\[|error:|panicked" "C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-core/1e7c582d-c03a-4db0-bd84-c7b3b7959ea0/tasks/bmkqol87f.output" 2>/dev/null; do sleep 2; done; echo "done-marker-seen"

Wait for spt-msg/spt-store test completion marker


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


<task-notification>
<task-id>bmkqol87f</task-id>
<tool-use-id>toolu_01U1PEMNS3z9etsYPBwCjAGb</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bmkqol87f.output</output-file>
<status>completed</status>
<summary>Background command "spt-msg lib tests + spt-store sessions tests (serial)" completed (exit code 0)</summary>
</task-notification>


Unnecessary poll task — I'll get notified on completion. Wait for bmkqol87f.


ToolSearch

Question: select:TaskStop


Question: select:TaskStop


Prompt:
<task-notification>
<task-id>bmkqol87f</task-id>
<tool-use-id>toolu_01U1PEMNS3z9etsYPBwCjAGb</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bmkqol87f.output</output-file>
<status>completed</status>
<summary>Background command "spt-msg lib tests + spt-store sessions tests (serial)" completed (exit code 0)</summary>
</task-notification>


spt-msg + spt-store exit 0. Stop stale poll; verify new unit test ran.


Successfully stopped task: b8yzt42tz (until [ -f /dev/null ] && grep -qE "test result:|error\[|error:|panicked" "C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-core/1e7c582d-c03a-4db0-bd84-c7b3b7959ea0/tasks/bmkqol87f.output" 2>/dev/null; do sleep 2; done; echo "done-marker-seen")


Bash

grep -E "test result:|start_ledgers_a_boot_session_row|sessions" "C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-core/1e7c582d-c03a-4db0-bd84-c7b3b7959ea0/tasks/bmkqol87f.output" | head -20

Confirm new unit test ran + results


test ready::tests::start_ledgers_a_boot_session_row ... ok
test result: ok. 32 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.25s
test sessions::tests::tolerant_of_garbage_lines ... ok
test sessions::tests::appends_and_reads_in_order ... ok
test sessions::tests::same_session_rebind_is_deduped ... ok
test sessions::tests::last_k_and_bounding ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 163 filtered out; finished in 0.80s


New unit ✓ (32 passed), sessions ✓ (4 passed). Now heavy E2Es serially (SPT_HOME is process-global → one test/binary, separate cargo invocations chained, `--test-threads=1`). Run the new int + ready-bringup siblings + the firsthost mirror.


cd /c/Users/decid/Documents/projects/spt-core
for t in ready_resume_ledger_e2e poll_envelope_e2e contract_e2e quickstart_e2e live_bind_firsthost_e2e; do
  echo "=========== $t ==========="
  CARGO_TARGET_DIR=target-seam cargo test -p spt --test $t -- --test-threads=1 2>&1 | grep -E "test result:|error\[|error:|panicked|FAILED|^test .* \.\.\. (ok|FAILED)"
  echo "EXIT_$t=${PIPESTATUS[0]}"
done

Run new int + ready/firsthost sibling E2Es serially


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


E2E batch running. Waiting for results before commit/ping.


1	=========== ready_resume_ledger_e2e ===========
2


Compiling. Wait for completion notification.


noop ack


waiting for E2E batch bh6luw552


Seam bar progress: clippy ✓, traceable EXIT=0 ✓, spt-msg lib ✓ (new unit passed), spt-store sessions ✓. E2E batch (new int + ready/firsthost siblings) compiling+running in background — awaiting completion before commit + doyle seam-green ping.


ScheduleWakeup

Fallback heartbeat while the E2E batch runs; task-completion notification is the primary wake.

Prompt: <<autonomous-loop-dynamic>>


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


Awaiting E2E batch. Will commit + ping doyle seam-green on green.


<task-notification>
<task-id>bh6luw552</task-id>
<tool-use-id>toolu_01SVTGR221r1NwcNLggv2hVh</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\1e7c582d-c03a-4db0-bd84-c7b3b7959ea0\tasks\bh6luw552.output</output-file>
<status>completed</status>
<summary>Background command "Run new int + ready/firsthost sibling E2Es serially" completed (exit code 0)</summary>
</task-notification>