240	    rec.controllable = controllable.or_else(|| prior.as_ref().and_then(|p| p.controllable));
241	    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;
242	    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
243	        .map_err(|e| BindError::Io(e.to_string()))?;
244
245	    // Session ledger (REQ-TERM-6): record this session as active so the digest
246	    // can span a later `/clear`|`/compact` boundary. The boot row dedups against
247	    // a same-session revive; a missing/unstamped session is not ledgered (the
248	    // digest spans only when there is a session to enumerate). Best-effort — a
249	    // ledger problem must never fail the bind.
250	    if !session_id.is_empty() {
251	        let _ = spt_store::sessions::append(
252	            &perch_path,
253	            &spt_store::sessions::SessionEntry {
254	                ts: spt_store::timefmt::now_rfc3339(),
255	                session_id: session_id.to_string(),
256	                trigger: spt_store::sessions::SessionTrigger::Boot,
257	            },
258	        );
259	    }
260
261	    // Keep an existing token across a revive; mint one for a fresh perch.
262	    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);
263	    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;
264	    Ok(token)
265	}
266
267	/// `api listen <id>` — consume the seed, bind the perch, then run the relay
268	/// loop (heir to `$LIVE start`'s Monitor-bound poll loop). `--once` drains and
269	/// exits (testability); otherwise blocks streaming messages to stdout.

    rec.controllable = controllable.or_else(|| prior.as_ref().and_then(|p| p.controllable));
    info::write_info(&perch_path, &rec).map_err(|e| BindError::Io(e.to_string()))?;
    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
        .map_err(|e| BindError::Io(e.to_string()))?;

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

    // Keep an existing token across a revive; mint one for a fresh perch.
    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);
    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;
    Ok(token)
}

/// `api listen <id>` — consume the seed, bind the perch, then run the relay
/// loop (heir to `$LIVE start`'s Monitor-bound poll loop). `--once` drains and
/// exits (testability); otherwise blocks streaming messages to stdout.


30	/// append so the file can never grow without bound (risk #2).
31	pub const MAX_LEDGER: usize = 64;
32
33	/// Why a session became active.
34	#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35	#[serde(rename_all = "lowercase")]
36	pub enum SessionTrigger {
37	    /// First bind of the perch (`api listen` / revive).
38	    Boot,
39	    /// A `/clear` boundary (`api boundary clear`).
40	    Clear,
41	    /// A `/compact` boundary (`api boundary compact`).
42	    Compact,
43	}
44
45	impl SessionTrigger {
46	    /// The lowercase token (matches the serde wire form) — used as the boundary
47	    /// marker `kind` in the spanned digest.
48	    pub fn token(self) -> &'static str {
49	        match self {
50	            SessionTrigger::Boot => "boot",
51	            SessionTrigger::Clear => "clear",
52	            SessionTrigger::Compact => "compact",
53	        }
54	    }
55	}
56
57	/// One ledger row: a `session_id` became active at `ts` via `trigger`.
58	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59	pub struct SessionEntry {
60	    /// RFC3339-UTC instant (see [`crate::timefmt`]).
61	    pub ts: String,
62	    /// The harness session id that became active.
63	    pub session_id: String,
64	    /// What caused the rotation.
65	    pub trigger: SessionTrigger,
66	}
67
68	/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable
69	/// line is skipped, the rest still load (the degrade-don't-crash store stance).
70	pub fn read_all(perch: &Path) -> Vec<SessionEntry> {
71	    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {
72	        return Vec::new();
73	    };
74	    s.lines()
75	        .filter(|l| !l.trim().is_empty())
76	        .filter_map(|l| serde_json::from_str(l).ok())
77	        .collect()
78	}
79
80	/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.
81	pub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {
82	    if k == 0 {
83	        return Vec::new();
84	    }
85	    let all = read_all(perch);
86	    let start = all.len().saturating_sub(k);
87	    all[start..].to_vec()
88	}
89
90	/// Append `entry`, deduping an immediate repeat of the same `session_id` (a
91	/// re-bind of the live session is not a new boundary) and bounding the ledger to
92	/// [`MAX_LEDGER`] (oldest rows rotate out). The whole bounded ledger is rewritten
93	/// atomically (it is tiny — ≤ `MAX_LEDGER` short rows — so a full rewrite avoids
94	/// partial-line corruption). Returns `true` if a row was written, `false` on a
95	/// same-session dedup.
96	// [impl->REQ-TERM-6]
97	pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {
98	    let mut entries = read_all(perch);
99	    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {
100	        return Ok(false); // same session re-bound — not a new boundary
101	    }
102	    entries.push(entry.clone());
103	    let start = entries.len().saturating_sub(MAX_LEDGER);
104	    let body: String = entries[start..]
105	        .iter()
106	        .map(|e| serde_json::to_string(e).unwrap_or_default())
107	        .map(|l| l + "\n")
108	        .collect();
109	    std::fs::create_dir_all(perch)?;
110	    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;
111	    Ok(true)
112	}
113
114	#[cfg(test)]
115	mod tests {
116	    use super::*;
117
118	    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {
119	        SessionEntry {

/// append so the file can never grow without bound (risk #2).
pub const MAX_LEDGER: usize = 64;

/// Why a session became active.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SessionTrigger {
    /// First bind of the perch (`api listen` / revive).
    Boot,
    /// A `/clear` boundary (`api boundary clear`).
    Clear,
    /// A `/compact` boundary (`api boundary compact`).
    Compact,
}

impl SessionTrigger {
    /// The lowercase token (matches the serde wire form) — used as the boundary
    /// marker `kind` in the spanned digest.
    pub fn token(self) -> &'static str {
        match self {
            SessionTrigger::Boot => "boot",
            SessionTrigger::Clear => "clear",
            SessionTrigger::Compact => "compact",
        }
    }
}

/// One ledger row: a `session_id` became active at `ts` via `trigger`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionEntry {
    /// RFC3339-UTC instant (see [`crate::timefmt`]).
    pub ts: String,
    /// The harness session id that became active.
    pub session_id: String,
    /// What caused the rotation.
    pub trigger: SessionTrigger,
}

/// Every ledger row in order (oldest→newest). Tolerant: a blank or unparseable
/// line is skipped, the rest still load (the degrade-don't-crash store stance).
pub fn read_all(perch: &Path) -> Vec<SessionEntry> {
    let Ok(s) = std::fs::read_to_string(perch.join(LEDGER_FILE)) else {
        return Vec::new();
    };
    s.lines()
        .filter(|l| !l.trim().is_empty())
        .filter_map(|l| serde_json::from_str(l).ok())
        .collect()
}

/// The last `k` rows (oldest→newest within the tail). `k == 0` ⇒ empty.
pub fn last_k(perch: &Path, k: usize) -> Vec<SessionEntry> {
    if k == 0 {
        return Vec::new();
    }
    let all = read_all(perch);
    let start = all.len().saturating_sub(k);
    all[start..].to_vec()
}

/// Append `entry`, deduping an immediate repeat of the same `session_id` (a
/// re-bind of the live session is not a new boundary) and bounding the ledger to
/// [`MAX_LEDGER`] (oldest rows rotate out). The whole bounded ledger is rewritten
/// atomically (it is tiny — ≤ `MAX_LEDGER` short rows — so a full rewrite avoids
/// partial-line corruption). Returns `true` if a row was written, `false` on a
/// same-session dedup.
// [impl->REQ-TERM-6]
pub fn append(perch: &Path, entry: &SessionEntry) -> io::Result<bool> {
    let mut entries = read_all(perch);
    if entries.last().map(|e| e.session_id.as_str()) == Some(entry.session_id.as_str()) {
        return Ok(false); // same session re-bound — not a new boundary
    }
    entries.push(entry.clone());
    let start = entries.len().saturating_sub(MAX_LEDGER);
    let body: String = entries[start..]
        .iter()
        .map(|e| serde_json::to_string(e).unwrap_or_default())
        .map(|l| l + "\n")
        .collect();
    std::fs::create_dir_all(perch)?;
    atomic_write_string(&perch.join(LEDGER_FILE), &body)?;
    Ok(true)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn entry(sid: &str, trigger: SessionTrigger) -> SessionEntry {
        SessionEntry {


Exact API confirmed. Check ready.rs imports + `session_id()` helper (used at ready.rs:77).


<owl_messages>
<owl_message from="doyle" priority="high">No resolve needed — c5cad76 is a clean linear descendant of your 981dea2 (d8e7761 purge-mint → 981dea2 your purge build → c5cad76 my 2 REQ mints). Just checkout/fast-forward to c5cad76, no conflict. Plan is right. Build on; ping at seam-green — this closes v0.12.0.</owl_message>
</owl_messages>

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


1	//! Ready-agent lifecycle: register a perch, listen, drain backlog, tear down.
2	//!
3	//! Clean-room re-expression of the sister `owl/poll.rs` startup + `setup.rs`
4	//! against the spt-store API, scoped to the M1 loopback model. The harness
5	//! coupling that bloats the sister `poll.rs` (stream EVENT emission, EVENT-PART
6	//! chunking, alarms, handoff, Psyche) is **not** M1 — it lands in M2/M3. M1's
7	//! ready agent is the minimal participant: a perch + a TCP listener + a spool
8	//! drain.
9	//!
10	//! Startup ([`ReadyAgent::start`]) is the REQ-MSG-3 contract:
11	//! 1. Create the perch dir; write `info.json` (`ready_agent` endpoint) + the
12	//!    `ready` marker.
13	//! 2. Bind a loopback listener and register its address (via [`PollListener`]).
14	//! 3. Drain the **non-deferred** spool backlog so messages that arrived while
15	//!    the agent was offline are delivered on startup. Deferred rows are left
16	//!    for the hook channel (REQ-HAZARD-DEFERRED-SURVIVE-DRAIN, covered M0).
17	//!
18	//! Teardown ([`ReadyAgent::soft_cleanup`]) honors REQ-HAZARD-SOFT-CLEANUP (6.2):
19	//! it removes **only** the `ready` marker and unregisters the address. The
20	//! spool and `info.json` are preserved so a restart resumes its backlog.
21	// [impl->REQ-MSG-3]
22
23	use spt_store::info::{self, InfoJson};
24	use spt_store::perch::{self, ParentHint};
25	use spt_store::spool;
26	use std::fs;
27	use std::path::PathBuf;
28	use std::time::Duration;
29
30	use crate::listener::PollListener;
31
32	/// Default poll window for [`ReadyAgent::recv`]; bounds each blocking accept so
33	/// the caller stays responsive (and can re-check spool / shut down between calls).
34	const RECV_POLL: Duration = Duration::from_millis(200);
35
36	/// A running ready agent: an owned perch + a registered loopback listener.
37	pub struct ReadyAgent {
38	    id: String,
39	    listener: PollListener,
40	    perch_path: PathBuf,
41	    closed: bool,
42	}
43
44	impl ReadyAgent {

//! Ready-agent lifecycle: register a perch, listen, drain backlog, tear down.
//!
//! Clean-room re-expression of the sister `owl/poll.rs` startup + `setup.rs`
//! against the spt-store API, scoped to the M1 loopback model. The harness
//! coupling that bloats the sister `poll.rs` (stream EVENT emission, EVENT-PART
//! chunking, alarms, handoff, Psyche) is **not** M1 — it lands in M2/M3. M1's
//! ready agent is the minimal participant: a perch + a TCP listener + a spool
//! drain.
//!
//! Startup ([`ReadyAgent::start`]) is the REQ-MSG-3 contract:
//! 1. Create the perch dir; write `info.json` (`ready_agent` endpoint) + the
//!    `ready` marker.
//! 2. Bind a loopback listener and register its address (via [`PollListener`]).
//! 3. Drain the **non-deferred** spool backlog so messages that arrived while
//!    the agent was offline are delivered on startup. Deferred rows are left
//!    for the hook channel (REQ-HAZARD-DEFERRED-SURVIVE-DRAIN, covered M0).
//!
//! Teardown ([`ReadyAgent::soft_cleanup`]) honors REQ-HAZARD-SOFT-CLEANUP (6.2):
//! it removes **only** the `ready` marker and unregisters the address. The
//! spool and `info.json` are preserved so a restart resumes its backlog.
// [impl->REQ-MSG-3]

use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;

use crate::listener::PollListener;

/// Default poll window for [`ReadyAgent::recv`]; bounds each blocking accept so
/// the caller stays responsive (and can re-check spool / shut down between calls).
const RECV_POLL: Duration = Duration::from_millis(200);

/// A running ready agent: an owned perch + a registered loopback listener.
pub struct ReadyAgent {
    id: String,
    listener: PollListener,
    perch_path: PathBuf,
    closed: bool,
}

impl ReadyAgent {


Already at c5cad76 (HEAD). No FF needed. Find `session_id()` source (used at ready.rs:77).


Bash

grep -n "session_id\|now_stamp" crates/spt-msg/src/ready.rs | head; echo "--- defns ---"; grep -rn "fn session_id\|fn now_stamp\|pub fn session_id" crates/spt-msg/src/ | head

Find session_id/now_stamp helper definitions


75:            &now_stamp(),
77:            &session_id(),
170:fn now_stamp() -> String {
179:fn session_id() -> String {
--- defns ---
crates/spt-msg/src/ready.rs:170:fn now_stamp() -> String {
crates/spt-msg/src/ready.rs:179:fn session_id() -> String {


168	/// Coarse creation timestamp (epoch seconds as a string). Opaque to readers —
169	/// `info.json` treats `started` as a producer-format string.
170	fn now_stamp() -> String {
171	    std::time::SystemTime::now()
172	        .duration_since(std::time::UNIX_EPOCH)
173	        .map(|d| d.as_secs().to_string())
174	        .unwrap_or_else(|_| "0".to_string())
175	}
176
177	/// Harness session id: honor `OWL_SESSION_ID` if set, else synthesize a
178	/// process-unique fallback. (M2 wires the real harness session id.)
179	fn session_id() -> String {
180	    if let Ok(s) = std::env::var("OWL_SESSION_ID") {
181	        if !s.is_empty() {
182	            return s;
183	        }
184	    }
185	    let nanos = std::time::SystemTime::now()
186	        .duration_since(std::time::UNIX_EPOCH)
187	        .map(|d| d.as_nanos())
188	        .unwrap_or(0);
189	    format!("sess-{}-{}", std::process::id(), nanos)
190	}
191
192	#[cfg(test)]
193	mod tests {
194	    use super::*;
195	    use crate::deliver;
196
197	    fn unique_id(prefix: &str) -> String {

/// Coarse creation timestamp (epoch seconds as a string). Opaque to readers —
/// `info.json` treats `started` as a producer-format string.
fn now_stamp() -> String {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".to_string())
}

/// Harness session id: honor `OWL_SESSION_ID` if set, else synthesize a
/// process-unique fallback. (M2 wires the real harness session id.)
fn session_id() -> String {
    if let Ok(s) = std::env::var("OWL_SESSION_ID") {
        if !s.is_empty() {
            return s;
        }
    }
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    format!("sess-{}-{}", std::process::id(), nanos)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::deliver;

    fn unique_id(prefix: &str) -> String {


`session_id()` always non-empty (synth fallback) but I'll guard anyway, mirroring establish_perch. Implement FIX1: capture sid once, ledger Boot after ready-marker write.


Edit

        // info.json first, so the record exists by the time the address is live.
        let prior = info::read_info(&perch_path);
        let mut rec = InfoJson::new(
            id,
            &now_stamp(),
            std::process::id(),
            &session_id(),
            "ready_agent",
        );

        // info.json first, so the record exists by the time the address is live.
        let prior = info::read_info(&perch_path);
        let sid = session_id();
        let mut rec = InfoJson::new(
            id,
            &now_stamp(),
            std::process::id(),
            &sid,
            "ready_agent",
        );


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

        // info.json first, so the record exists by the time the address is live.
        let prior = info::read_info(&perch_path);
        let mut rec = InfoJson::new(
            id,
            &now_stamp(),
            std::process::id(),
            &session_id(),
            "ready_agent",
        );

        // info.json first, so the record exists by the time the address is live.
        let prior = info::read_info(&perch_path);
        let sid = session_id();
        let mut rec = InfoJson::new(
            id,
            &now_stamp(),
            std::process::id(),
            &sid,
            "ready_agent",
        );

let subnets = spt_store::subnet::SubnetStore::load();


        // ready marker — the liveness signal senders check.
        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
            .map_err(|e| format!("Failed to write ready marker: {}", e))?;

        // Bind + register the listener address. Blocking with a short poll so

        // ready marker — the liveness signal senders check.
        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
            .map_err(|e| format!("Failed to write ready marker: {}", e))?;

        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind
        // writes info.json directly and — unlike the live `establish_perch` path
        // (startup.rs:250) — never ledgered, so an offline ready perch carried
        // ZERO session rows and the picker's offline Resume-from-history (which
        // gates on ledger rows) never offered it. Append a Boot row here, mirroring
        // the live path: the boot row dedups against a same-session re-bind, an
        // empty session is not ledgered, and a ledger problem must never fail the
        // bind (best-effort).
        // [impl->REQ-READY-AGENT-RESUME]
        if !sid.is_empty() {
            let _ = spt_store::sessions::append(
                &perch_path,
                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::now_rfc3339(),
                    session_id: sid.clone(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                },
            );
        }

        // Bind + register the listener address. Blocking with a short poll so


        // ready marker — the liveness signal senders check.
        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
            .map_err(|e| format!("Failed to write ready marker: {}", e))?;

        // Bind + register the listener address. Blocking with a short poll so

        // ready marker — the liveness signal senders check.
        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), "")
            .map_err(|e| format!("Failed to write ready marker: {}", e))?;

        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind
        // writes info.json directly and — unlike the live `establish_perch` path
        // (startup.rs:250) — never ledgered, so an offline ready perch carried
        // ZERO session rows and the picker's offline Resume-from-history (which
        // gates on ledger rows) never offered it. Append a Boot row here, mirroring
        // the live path: the boot row dedups against a same-session re-bind, an
        // empty session is not ledgered, and a ledger problem must never fail the
        // bind (best-effort).
        // [impl->REQ-READY-AGENT-RESUME]
        if !sid.is_empty() {
            let _ = spt_store::sessions::append(
                &perch_path,
                &spt_store::sessions::SessionEntry {
                    ts: spt_store::timefmt::now_rfc3339(),
                    session_id: sid.clone(),
                    trigger: spt_store::sessions::SessionTrigger::Boot,
                },
            );
        }

        // Bind + register the listener address. Blocking with a short poll so

+        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind

+        // writes info.json directly and — unlike the live `establish_perch` path

+        // (startup.rs:250) — never ledgered, so an offline ready perch carried

+        // ZERO session rows and the picker's offline Resume-from-history (which

+        // gates on ledger rows) never offered it. Append a Boot row here, mirroring

+        // the live path: the boot row dedups against a same-session re-bind, an

+        // empty session is not ledgered, and a ledger problem must never fail the

+        // [impl->REQ-READY-AGENT-RESUME]

+            let _ = spt_store::sessions::append(

+                &spt_store::sessions::SessionEntry {

+                    ts: spt_store::timefmt::now_rfc3339(),

+                    session_id: sid.clone(),

+                    trigger: spt_store::sessions::SessionTrigger::Boot,

let listener = PollListener::bind_blocking(id, RECV_POLL)?;


FIX1 done. Now FIX2 — VERIFY-AT-CODE (B4-style). Read the psyche-host gate (cli.rs:5045) + cmd_endpoint_run + --resume.


Bash

grep -n "psyche_init\|fn cmd_endpoint_run\|launch_harness_brokered\|--resume\|resume\b\|fn launch_harness" crates/spt/src/cli.rs | head -50

Locate endpoint run + psyche_init gate + resume


265:        /// shortcut can bake create-vs-resume). Conflicts with --resume.
266:        #[arg(long, conflicts_with = "resume")]
270:        resume: Option<String>,
1111:                // flag symmetry so a shortcut can bake create-vs-resume); at
1112:                // dispatch it only matters as the ABSENCE of --resume, so it is
1113:                // not threaded further. clap already conflicts it with --resume.
1115:                resume,
1122:                // only matters as the absence of `--resume`, so it is not
1125:                    cmd_endpoint_run(&adapter, &id, resume, start, attach, view)
1253:pub(crate) fn cmd_endpoint_run(
1256:    resume: Option<String>,
1290:    // A fresh bringup mints a provisional spawn-time session id; --resume carries
1292:    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);
1306:    match spt_daemon::harnesshost::launch_harness_brokered_in(
5045:`[session.psyche_init]` (the Psyche-spawn seam). A manifest without that
10565:        // explicitly NOT it; the Psyche seam ([session.psyche_init]) is what
10574:            live.contains("[session.psyche_init]"),


1253	pub(crate) fn cmd_endpoint_run(
1254	    adapter: &str,
1255	    id: &str,
1256	    resume: Option<String>,
1257	    start: bool,
1258	    // `--attach` is the default terminal action; the bool is accepted for flag
1259	    // symmetry but `start`/`view` are what branch (attach = neither).
1260	    _attach: bool,
1261	    view: bool,
1262	) -> i32 {
1263	    // id charset (the harness binds to exactly this id via argv).
1264	    // [impl->REQ-HAZARD-ID-CHARSET]
1265	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
1266	        eprintln!("INVALID_ID:{id}: {e}");
1267	        return 2;
1268	    }
1269	    // Resolve the harness adapter option (`<adapter>[:profile]`) through the
1270	    // merged view (split → parent lookup → overlay), exactly like shell spawn.
1271	    let adapters_dir = spt_store::perch::adapters_dir();
1272	    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
1273	        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
1274	        Ok(_) => {
1275	            eprintln!(
1276	                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
1277	                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
1278	            );
1279	            return 1;
1280	        }
1281	        Err(e) => {
1282	            eprintln!(
1283	                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
1284	                 harness adapter / valid profile on this node ({e}; spt adapter list)"
1285	            );
1286	            return 1;
1287	        }
1288	    };
1289
1290	    // A fresh bringup mints a provisional spawn-time session id; --resume carries
1291	    // a prior one (the harness reports the real id post-spawn either way).
1292	    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);
1293
1294	    if let Err(e) = spt_daemon::daemon::ensure_running() {
1295	        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
1296	        return 1;
1297	    }
1298	    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
1299	    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
1300	    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
1301	    // session history, and digest source relative to cwd. A failure to read the
1302	    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
1303	    let project_cwd = std::env::current_dir()
1304	        .ok()
1305	        .map(|p| p.to_string_lossy().into_owned());
1306	    match spt_daemon::harnesshost::launch_harness_brokered_in(
1307	        &spt_daemon::endpoint::broker_socket_name(),
1308	        id,
1309	        adapter,
1310	        &session_id,
1311	        &manifest,
1312	        project_cwd.as_deref(),
1313	    ) {
1314	        Ok(spawned) => {
1315	            eprintln!(
1316	                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
1317	                 (harness binds its perch on startup)",
1318	                spawned.pid
1319	            );
1320	        }
1321	        Err(e) => {
1322	            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
1323	            return 1;
1324	        }
1325	    }
1326
1327	    // Terminal action: start (return now) | view (read-only attach) | attach
1328	    // (default). Attach/view route through the SAME rc pump as `spt rc`.
1329	    if start {
1330	        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
1331	        return 0;
1332	    }
1333	    match crate::rc::run_attach(id, rc_intent(view, false)) {
1334	        Ok(()) => 0,
1335	        Err(e) => {
1336	            eprintln!("RC_FAIL:{id}: {e}");
1337	            1
1338	        }
1339	    }
1340	}
1341
1342	/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move

pub(crate) fn cmd_endpoint_run(
    adapter: &str,
    id: &str,
    resume: Option<String>,
    start: bool,
    // `--attach` is the default terminal action; the bool is accepted for flag
    // symmetry but `start`/`view` are what branch (attach = neither).
    _attach: bool,
    view: bool,
) -> i32 {
    // id charset (the harness binds to exactly this id via argv).
    // [impl->REQ-HAZARD-ID-CHARSET]
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return 2;
    }
    // Resolve the harness adapter option (`<adapter>[:profile]`) through the
    // merged view (split → parent lookup → overlay), exactly like shell spawn.
    let adapters_dir = spt_store::perch::adapters_dir();
    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
        Ok(_) => {
            eprintln!(
                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
            );
            return 1;
        }
        Err(e) => {
            eprintln!(
                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
                 harness adapter / valid profile on this node ({e}; spt adapter list)"
            );
            return 1;
        }
    };

    // A fresh bringup mints a provisional spawn-time session id; --resume carries
    // a prior one (the harness reports the real id post-spawn either way).
    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);

    if let Err(e) = spt_daemon::daemon::ensure_running() {
        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
        return 1;
    }
    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
    // session history, and digest source relative to cwd. A failure to read the
    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
    let project_cwd = std::env::current_dir()
        .ok()
        .map(|p| p.to_string_lossy().into_owned());
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
        project_cwd.as_deref(),
    ) {
        Ok(spawned) => {
            eprintln!(
                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
                 (harness binds its perch on startup)",
                spawned.pid
            );
        }
        Err(e) => {
            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
            return 1;
        }
    }

    // Terminal action: start (return now) | view (read-only attach) | attach
    // (default). Attach/view route through the SAME rc pump as `spt rc`.
    if start {
        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
        return 0;
    }
    match crate::rc::run_attach(id, rc_intent(view, false)) {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("RC_FAIL:{id}: {e}");
            1
        }
    }
}

/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move


cmd_endpoint_run is fully type-agnostic — no psyche_init check, passes manifest through, `--resume` carries session_id. The psyche-host decision lives downstream in `launch_harness_brokered_in`. Verify it keys on psyche_init.


grep -rn "psyche_init\|fn launch_harness_brokered_in\|spawn_psyche\|psyche_host\|host_psyche\|fn spawn_live_host\|psyche" crates/spt-daemon/src/harnesshost.rs | head -40

Verify psyche-host gates on psyche_init in harnesshost


144:pub fn launch_harness_brokered_in(


harnesshost has no psyche logic. Psyche-host decided elsewhere (at perch bind). Find the actual psyche-host gate across the daemon.


grep -rn "psyche_init\|spawn_live_host\|host_one\|LIVEHOST\|psyche_host" crates/spt-daemon/src/*.rs | grep -iv "test" | head -40

Find psyche-host decision keyed on psyche_init


crates/spt-daemon/src/brainproc.rs:184:    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`
crates/spt-daemon/src/brainproc.rs:230:    let _ = crate::livehost::spawn_live_host(Arc::new(AtomicBool::new(false)), reason);
crates/spt-daemon/src/lifecycle.rs:78:    /// `[session.psyche_init]`); `None` for an absent manifest or a plain relay
crates/spt-daemon/src/lifecycle.rs:106:        manifest.session.psyche_init.as_ref()?;
crates/spt-daemon/src/lifecycle.rs:612:             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates/spt-daemon/src/lifecycle.rs:632:             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n\n\
crates/spt-daemon/src/livehost.rs:54:/// (REQ-HAZARD-LIVEHOST-NONRESIDENT). Chosen as ~one reconcile interval (5s): a
crates/spt-daemon/src/livehost.rs:64:// [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:78:// [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:95:    /// When [`host_one`] spawned this endpoint's Psyche — the residency-grace
crates/spt-daemon/src/livehost.rs:96:    /// anchor (REQ-HAZARD-LIVEHOST-NONRESIDENT).
crates/spt-daemon/src/livehost.rs:107:/// `psyche_host_error` stamp stays visible and the phantom nested perch stays
crates/spt-daemon/src/livehost.rs:111:/// permanently-broken one is stamped + quiet (REQ-HAZARD-LIVEHOST-NONRESIDENT).
crates/spt-daemon/src/livehost.rs:122:    /// re-spawned every tick (REQ-HAZARD-LIVEHOST-NONRESIDENT).
crates/spt-daemon/src/livehost.rs:169:    /// it is not tabled. [`host_one`] stamps `hosted_at` at spawn time.
crates/spt-daemon/src/livehost.rs:170:    // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:185:    // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:195:    /// re-hosting until it expires). // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:205:    /// entries are pruned. // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:294:        // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT] RESIDENCY confirm: the parent is
crates/spt-daemon/src/livehost.rs:297:        // v0.8.1 spawn-Err stamp never fired), stamp the parent psyche_host_error,
crates/spt-daemon/src/livehost.rs:308:        // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT] a just-failed residency endpoint is
crates/spt-daemon/src/livehost.rs:346:            continue; // manifest declares no [session.psyche_init]
crates/spt-daemon/src/livehost.rs:348:        host_one(set, &id, lifecycle, info.session_id, reason);
crates/spt-daemon/src/livehost.rs:357:fn host_one(
crates/spt-daemon/src/livehost.rs:371:            eprintln!("LIVEHOST_PSYCHE:{id} pid={}", h.pid);
crates/spt-daemon/src/livehost.rs:377:            // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:388:            // host_one returns without tabling, so the 5s reconcile retries and each
crates/spt-daemon/src/livehost.rs:391:            // [impl->REQ-HAZARD-LIVEHOST-BOOT-RACE]
crates/spt-daemon/src/livehost.rs:392:            eprintln!("LIVEHOST_PSYCHE_FAIL:{id}: {e}");
crates/spt-daemon/src/livehost.rs:393:            let _ = spt_store::info::set_psyche_host_error(&perch, Some(&e.to_string()));
crates/spt-daemon/src/livehost.rs:405:    // the `{id}-psyche` perch's liveness against (REQ-HAZARD-LIVEHOST-NONRESIDENT).
crates/spt-daemon/src/livehost.rs:418:/// window — the F-010 fix (REQ-HAZARD-LIVEHOST-NONRESIDENT). A child that spawned
crates/spt-daemon/src/livehost.rs:431:///   failure → stamp the PARENT perch `psyche_host_error{reason: host not resident
crates/spt-daemon/src/livehost.rs:437:// [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:452:            // (The clear was deliberately MOVED here from host_one's spawn-Ok leg:
crates/spt-daemon/src/livehost.rs:455:            // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
crates/spt-daemon/src/livehost.rs:456:            let _ = spt_store::info::set_psyche_host_error(&parent_perch, None);
crates/spt-daemon/src/livehost.rs:466:            // (liveness stays daemon-authoritative; the SIGNAL is psyche_host_error,
crates/spt-daemon/src/livehost.rs:468:            let _ = spt_store::info::set_psyche_host_error(&parent_perch, Some(&reason));


Found the gate: livehost.rs:346 `continue; // manifest declares no [session.psyche_init]`. Read 325-355 to confirm the reconcile skips ready endpoints.


325	        let Some(adapter) = info.adapter.as_deref() else {
326	            continue; // adapterless endpoint — never live-capable
327	        };
328	        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
329	        else {
330	            continue; // deregistered adapter: nothing to host
331	        };
332	        // [impl->REQ-INSTALL-11] resolve the Psyche role program against the
333	        // adapter install dir — the registry record's precise `source_dir`, the
334	        // same dir Feature E's api seam uses (mod.rs::resolve_ctx_manifest). The
335	        // daemon-host site is the heir to the retired in-process spawn's
336	        // install-dir resolution: without it the daemon-hosted Psyche resolves a
337	        // copy-mode psyche binary only via PATH, not its install dir.
338	        let parent = adapter.split(':').next().unwrap_or(adapter);
339	        let install_dir = registered
340	            .iter()
341	            .find(|(rec, _)| rec.name == parent)
342	            .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir));
343	        let Some(lifecycle) =
344	            BrainLifecycle::with_config_in(&manifest, &id, cfg.clone(), install_dir.as_deref())
345	        else {
346	            continue; // manifest declares no [session.psyche_init]
347	        };
348	        host_one(set, &id, lifecycle, info.session_id, reason);
349	    }
350	}
351
352	/// Host one live endpoint: sweep a stale signoff (3.2), spawn the Psyche (the
353	/// daemon online-stamp makes liveness daemon-authoritative), then start the
354	/// config-paced pulse driver on its own supervised thread with a held stop flag.

        let Some(adapter) = info.adapter.as_deref() else {
            continue; // adapterless endpoint — never live-capable
        };
        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
        else {
            continue; // deregistered adapter: nothing to host
        };
        // [impl->REQ-INSTALL-11] resolve the Psyche role program against the
        // adapter install dir — the registry record's precise `source_dir`, the
        // same dir Feature E's api seam uses (mod.rs::resolve_ctx_manifest). The
        // daemon-host site is the heir to the retired in-process spawn's
        // install-dir resolution: without it the daemon-hosted Psyche resolves a
        // copy-mode psyche binary only via PATH, not its install dir.
        let parent = adapter.split(':').next().unwrap_or(adapter);
        let install_dir = registered
            .iter()
            .find(|(rec, _)| rec.name == parent)
            .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir));
        let Some(lifecycle) =
            BrainLifecycle::with_config_in(&manifest, &id, cfg.clone(), install_dir.as_deref())
        else {
            continue; // manifest declares no [session.psyche_init]
        };
        host_one(set, &id, lifecycle, info.session_id, reason);
    }
}

/// Host one live endpoint: sweep a stale signoff (3.2), spawn the Psyche (the
/// daemon online-stamp makes liveness daemon-authoritative), then start the
/// config-paced pulse driver on its own supervised thread with a held stop flag.


FIX2 = **SUBSUMED, zero code** confirmed at code:
- `cmd_endpoint_run` type-agnostic — no psyche check, passes manifest through, `--resume` carries session_id.
- Psyche-host gate is the daemon reconcile loop (livehost.rs:343-347): `BrainLifecycle::with_config_in` returns `None` when manifest has no `[session.psyche_init]` (lifecycle.rs:106) → `continue` → NO psyche hosted. Ready endpoint = poll-listener only.

Report to doyle + read ready.rs test module to mirror style.


$OWL send doyle <<'EOF'
FIX2 VERDICT = SUBSUMED (verified-at-code, B4-style, zero code). Chain: cmd_endpoint_run (cli.rs:1253) is type-agnostic — no psyche_init check, resolves+passes manifest, --resume carries session_id into launch_harness_brokered_in (which has NO psyche logic). The psyche-host gate is the daemon RECONCILE loop livehost.rs:343-347: BrainLifecycle::with_config_in returns None when manifest has no [session.psyche_init] (lifecycle.rs:106 `psyche_init.as_ref()?`) → `continue` → NO psyche hosted. So a no-psyche_init (ready) endpoint via endpoint run = poll-listener only, exactly as operator specified. Only residual = FIX1 (ledger start_homed) DONE. Building unit+int+CONTEXT+activate+seam now.
EOF

Report FIX2 subsumed to doyle


➜ SENT:doyle


205	    }
206	    let nanos = std::time::SystemTime::now()
207	        .duration_since(std::time::UNIX_EPOCH)
208	        .map(|d| d.as_nanos())
209	        .unwrap_or(0);
210	    format!("sess-{}-{}", std::process::id(), nanos)
211	}
212
213	#[cfg(test)]
214	mod tests {
215	    use super::*;
216	    use crate::deliver;
217
218	    fn unique_id(prefix: &str) -> String {
219	        let nanos = std::time::SystemTime::now()
220	            .duration_since(std::time::UNIX_EPOCH)
221	            .map(|d| d.as_nanos())
222	            .unwrap_or(0);
223	        format!("{}-{}-{}", prefix, std::process::id(), nanos)
224	    }
225
226	    // [unit->REQ-MSG-3] start registers a reachable, resolvable perch.
227	    #[test]
228	    fn start_registers_resolvable_perch() {
229	        crate::test_home::isolate();
230	        let id = unique_id("test-ready-start");
231	        let (agent, backlog) = ReadyAgent::start(&id).unwrap();
232	        assert!(backlog.is_empty());
233
234	        // Resolvable + online from a sender's point of view.
235	        assert!(deliver::is_online(&id));
236	        assert!(deliver::perch_exists(&id));
237
238	        // info.json + ready marker on disk.
239	        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
240	        assert!(info::read_info(&perch_path).is_some());
241	        assert!(perch::resolve_ready_file(&id, ParentHint::Infer).exists());
242	        drop(agent);
243	    }
244
245	    // [unit->REQ-MSG-3] backlog spooled while offline drains on startup.
246	    #[test]
247	    fn backlog_drains_on_start() {
248	        crate::test_home::isolate();
249	        let owlery = perch::owlery_dir();
250	        let id = unique_id("test-ready-backlog");
251	        // Pre-create an offline perch and spool a message to it.
252	        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
253	        fs::create_dir_all(&perch_path).unwrap();
254	        let rec = InfoJson::new(&id, "now", std::process::id(), "sess", "ready_agent");
255	        info::write_info(&perch_path, &rec).unwrap();
256	        assert_eq!(
257	            deliver::deliver(&id, "sender", "while you slept", &owlery),
258	            deliver::DeliveryPath::Spool
259	        );
260
261	        // Starting drains it.
262	        let (_agent, backlog) = ReadyAgent::start(&id).unwrap();
263	        assert_eq!(backlog.len(), 1);
264	        assert_eq!(backlog[0].0, "sender");
265	        assert!(backlog[0].1.contains("while you slept"));
266	    }
267
268	    // [unit->REQ-MSG-3] a live TCP message is received via recv().
269	    #[test]
270	    fn recv_gets_live_tcp_message() {
271	        crate::test_home::isolate();
272	        let owlery = perch::owlery_dir();
273	        let id = unique_id("test-ready-recv");
274	        let (agent, _) = ReadyAgent::start(&id).unwrap();
275
276	        let owlery2 = owlery.clone();
277	        let target = id.clone();
278	        let h = std::thread::spawn(move || {
279	            std::thread::sleep(Duration::from_millis(30));
280	            deliver::deliver(&target, "peer", "hello live", &owlery2)
281	        });
282
283	        let mut got = None;
284	        for _ in 0..25 {
285	            if let Some(m) = agent.recv() {
286	                got = Some(m);
287	                break;
288	            }
289	        }
290	        assert_eq!(h.join().unwrap(), deliver::DeliveryPath::Tcp);
291	        assert_eq!(got, Some(("peer".to_string(), "hello live".to_string())));
292	        drop(agent);
293	    }
294
295	    // [unit->REQ-HAZARD-SOFT-CLEANUP] soft_cleanup removes only the ready marker;
296	    // spool (deferred row) and info.json are preserved.
297	    #[test]
298	    fn soft_cleanup_preserves_spool_and_info() {
299	        crate::test_home::isolate();
300	        let id = unique_id("test-ready-soft");
301	        let (mut agent, _) = ReadyAgent::start(&id).unwrap();
302	        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
303
304	        // A deferred row survives the startup drain — use it to prove the spool
305	        // is preserved across cleanup.
306	        spool::spool_message_deferred_at(&perch_path, "hooker", "deferred body").unwrap();
307	        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
308
309	        agent.soft_cleanup();
310
311	        // ready marker gone...
312	        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());
313	        // ...but spool row + info.json preserved.
314	        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
315	        assert!(info::read_info(&perch_path).is_some());
316	    }
317	}
318

    }
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    format!("sess-{}-{}", std::process::id(), nanos)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::deliver;

    fn unique_id(prefix: &str) -> String {
        let nanos = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0);
        format!("{}-{}-{}", prefix, std::process::id(), nanos)
    }

    // [unit->REQ-MSG-3] start registers a reachable, resolvable perch.
    #[test]
    fn start_registers_resolvable_perch() {
        crate::test_home::isolate();
        let id = unique_id("test-ready-start");
        let (agent, backlog) = ReadyAgent::start(&id).unwrap();
        assert!(backlog.is_empty());

        // Resolvable + online from a sender's point of view.
        assert!(deliver::is_online(&id));
        assert!(deliver::perch_exists(&id));

        // info.json + ready marker on disk.
        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
        assert!(info::read_info(&perch_path).is_some());
        assert!(perch::resolve_ready_file(&id, ParentHint::Infer).exists());
        drop(agent);
    }

    // [unit->REQ-MSG-3] backlog spooled while offline drains on startup.
    #[test]
    fn backlog_drains_on_start() {
        crate::test_home::isolate();
        let owlery = perch::owlery_dir();
        let id = unique_id("test-ready-backlog");
        // Pre-create an offline perch and spool a message to it.
        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
        fs::create_dir_all(&perch_path).unwrap();
        let rec = InfoJson::new(&id, "now", std::process::id(), "sess", "ready_agent");
        info::write_info(&perch_path, &rec).unwrap();
        assert_eq!(
            deliver::deliver(&id, "sender", "while you slept", &owlery),
            deliver::DeliveryPath::Spool
        );

        // Starting drains it.
        let (_agent, backlog) = ReadyAgent::start(&id).unwrap();
        assert_eq!(backlog.len(), 1);
        assert_eq!(backlog[0].0, "sender");
        assert!(backlog[0].1.contains("while you slept"));
    }

    // [unit->REQ-MSG-3] a live TCP message is received via recv().
    #[test]
    fn recv_gets_live_tcp_message() {
        crate::test_home::isolate();
        let owlery = perch::owlery_dir();
        let id = unique_id("test-ready-recv");
        let (agent, _) = ReadyAgent::start(&id).unwrap();

        let owlery2 = owlery.clone();
        let target = id.clone();
        let h = std::thread::spawn(move || {
            std::thread::sleep(Duration::from_millis(30));
            deliver::deliver(&target, "peer", "hello live", &owlery2)
        });

        let mut got = None;
        for _ in 0..25 {
            if let Some(m) = agent.recv() {
                got = Some(m);
                break;
            }
        }
        assert_eq!(h.join().unwrap(), deliver::DeliveryPath::Tcp);
        assert_eq!(got, Some(("peer".to_string(), "hello live".to_string())));
        drop(agent);
    }

    // [unit->REQ-HAZARD-SOFT-CLEANUP] soft_cleanup removes only the ready marker;
    // spool (deferred row) and info.json are preserved.
    #[test]
    fn soft_cleanup_preserves_spool_and_info() {
        crate::test_home::isolate();
        let id = unique_id("test-ready-soft");
        let (mut agent, _) = ReadyAgent::start(&id).unwrap();
        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);

        // A deferred row survives the startup drain — use it to prove the spool
        // is preserved across cleanup.
        spool::spool_message_deferred_at(&perch_path, "hooker", "deferred body").unwrap();
        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);

        agent.soft_cleanup();

        // ready marker gone...
        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());
        // ...but spool row + info.json preserved.
        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
        assert!(info::read_info(&perch_path).is_some());
    }
}


Test module style clear. Add unit test: ready bind writes a Boot ledger row.


Edit

        // ready marker gone...
        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());
        // ...but spool row + info.json preserved.
        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
        assert!(info::read_info(&perch_path).is_some());
    }
}

        // ready marker gone...
        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());
        // ...but spool row + info.json preserved.
        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
        assert!(info::read_info(&perch_path).is_some());
    }

    // [unit->REQ-READY-AGENT-RESUME] a ready bind ledgers a Boot session row, so
    // an OFFLINE ready perch carries resume rows and the picker's
    // Resume-from-history offers it (closing the live-only gap). The session row
    // SURVIVES soft_cleanup (only the ready marker is removed) — the offline state
    // the picker reads.
    #[test]
    fn start_ledgers_a_boot_session_row() {
        crate::test_home::isolate();
        let id = unique_id("test-ready-ledger");
        let (mut agent, _) = ReadyAgent::start(&id).unwrap();
        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);

        // The bind wrote exactly one Boot row keyed on the agent's session id.
        let rows = spt_store::sessions::read_all(&perch_path);
        assert_eq!(rows.len(), 1, "ready bind must write one ledger row");
        assert_eq!(rows[0].trigger, spt_store::sessions::SessionTrigger::Boot);
        assert!(!rows[0].session_id.is_empty());

        // Goes offline → the resume rows the picker gates on persist.
        agent.soft_cleanup();
        let offline_rows = spt_store::sessions::read_all(&perch_path);
        assert_eq!(offline_rows.len(), 1, "ledger survives soft_cleanup (offline)");
    }
}


        // ready marker gone...
        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());
        // ...but spool row + info.json preserved.
        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
        assert!(info::read_info(&perch_path).is_some());
    }
}

        // ready marker gone...
        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());
        // ...but spool row + info.json preserved.
        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
        assert!(info::read_info(&perch_path).is_some());
    }

    // [unit->REQ-READY-AGENT-RESUME] a ready bind ledgers a Boot session row, so
    // an OFFLINE ready perch carries resume rows and the picker's
    // Resume-from-history offers it (closing the live-only gap). The session row
    // SURVIVES soft_cleanup (only the ready marker is removed) — the offline state
    // the picker reads.
    #[test]
    fn start_ledgers_a_boot_session_row() {
        crate::test_home::isolate();
        let id = unique_id("test-ready-ledger");
        let (mut agent, _) = ReadyAgent::start(&id).unwrap();
        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);

        // The bind wrote exactly one Boot row keyed on the agent's session id.
        let rows = spt_store::sessions::read_all(&perch_path);
        assert_eq!(rows.len(), 1, "ready bind must write one ledger row");
        assert_eq!(rows[0].trigger, spt_store::sessions::SessionTrigger::Boot);
        assert!(!rows[0].session_id.is_empty());

        // Goes offline → the resume rows the picker gates on persist.
        agent.soft_cleanup();
        let offline_rows = spt_store::sessions::read_all(&perch_path);
        assert_eq!(offline_rows.len(), 1, "ledger survives soft_cleanup (offline)");
    }
}

+    // [unit->REQ-READY-AGENT-RESUME] a ready bind ledgers a Boot session row, so

+    // an OFFLINE ready perch carries resume rows and the picker's

+    // Resume-from-history offers it (closing the live-only gap). The session row

+    // SURVIVES soft_cleanup (only the ready marker is removed) — the offline state

+    fn start_ledgers_a_boot_session_row() {

+        let id = unique_id("test-ready-ledger");

+        let (mut agent, _) = ReadyAgent::start(&id).unwrap();

+        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);

+        // The bind wrote exactly one Boot row keyed on the agent's session id.

+        let rows = spt_store::sessions::read_all(&perch_path);

+        assert_eq!(rows.len(), 1, "ready bind must write one ledger row");

+        assert_eq!(rows[0].trigger, spt_store::sessions::SessionTrigger::Boot);

+        assert!(!rows[0].session_id.is_empty());

+        // Goes offline → the resume rows the picker gates on persist.

+        let offline_rows = spt_store::sessions::read_all(&perch_path);

+        assert_eq!(offline_rows.len(), 1, "ledger survives soft_cleanup (offline)");


Unit done. Now the int test. Find existing endpoint-run / livehost E2Es to mirror (these need a scratch target dir per the milestone lessons).


Bash

ls crates/spt-daemon/tests/ crates/spt/tests/ 2>/dev/null; echo "=== endpoint_run / psyche_init / NO psyche refs in tests ==="; grep -rln "endpoint run\|cmd_endpoint_run\|psyche_init\|LIVEHOST_PSYCHE\|no_psyche\|ready_agent\|launch_harness_brokered" crates/*/tests/ | head

Locate endpoint/livehost integration test files


crates/spt-daemon/tests/:
access.rs
attach.rs
brain_swap.rs
broker.rs
budget.rs
daemon_e2e.rs
daemon_lifecycle_real_brain.rs
digest.rs
dispatch.rs
handoff.rs
idempotent.rs
mesh.rs
netbroker.rs
netstream.rs
notifsync.rs
pairjoin.rs
presence.rs
propagate.rs
pump.rs
pumpdeadline.rs
replicate.rs
reseed.rs
resume.rs
rosterprop.rs
seedproofx.rs
shellchan.rs
sync.rs
two_origin_spanning.rs
twohost.rs
wanmsg.rs
xfer.rs

crates/spt/tests/:
brain_respawn_rename.rs
brain_restart_psyche_dup_e2e.rs
brain_split.rs
brain_survive.rs
common
contract_e2e.rs
drive_e2e.rs
gateway_e2e.rs
gateway_owner_shell_e2e.rs
live_bind_firsthost_e2e.rs
live_firsthost_e2e.rs
live_resolve_e2e.rs
livehost_bootgate_e2e.rs
livehost_bootrace_e2e.rs
livehost_nonresident_e2e.rs
livehost_psyche_fail_e2e.rs
n1_compat.rs
n1_pairing.rs
notify_shell_e2e.rs
oneliner_e2e.rs
poll_envelope_e2e.rs
quickstart_e2e.rs
release_verify_e2e.rs
shell_actgate_e2e.rs
shell_e2e.rs
shell_sleepwake_e2e.rs
tunnel_e2e.rs
unhost_psyche_reap_e2e.rs
=== endpoint_run / psyche_init / NO psyche refs in tests ===
crates/spt-daemon/tests/access.rs
crates/spt-daemon/tests/attach.rs
crates/spt-daemon/tests/broker.rs
crates/spt-daemon/tests/daemon_e2e.rs
crates/spt-daemon/tests/daemon_lifecycle_real_brain.rs
crates/spt-daemon/tests/dispatch.rs
crates/spt-daemon/tests/mesh.rs
crates/spt-daemon/tests/pump.rs
crates/spt-daemon/tests/twohost.rs
crates/spt-daemon/tests/wanmsg.rs


Rich E2E suite. Read my recent `livehost_bootgate_e2e.rs` (freshest endpoint-run + psyche-host-assertion pattern) + the daemon `resume.rs` to mirror.


<owl_messages>
<owl_message from="doyle" priority="high">FIX2=SUBSUMED accepted — clean verified-at-code (B4-style): the psyche-host gate IS the reconcile loop (with_config_in → None on no psyche_init → no host), endpoint run stays type-agnostic, --resume carries the session. Exactly the operator's "type = manifest concern, no new mode." FIX1 ledger done. Finish unit + int + the CONTEXT clarification + activate [impl,unit,int]+tag + full seam bar. Ping at seam-green → I gate → deployah → v0.12.0 (the last item).

Heads-up: I'm communing + the operator is ending this session — I resume next session. Your seam-green ping will be waiting; I'll gate it first thing. Build on.</owl_message>
</owl_messages>

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


1	//! B5 boot-liveness-gate int (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE) — a COLD
2	//! daemon start after an unclean stop must NOT revive a phantom Psyche for a
3	//! dead-but-online-latched perch.
4	//!
5	//! The scenario the operator hit: a prior daemon life left an spt-hosted
6	//! (`controllable==Some(true)`) `live_agent` perch stamped `status=online` on disk,
7	//! then the daemon died uncleanly (the one-way latch was never cleared). On the
8	//! NEXT cold start the broker has NO session for it (the harness is long gone), yet
9	//! the old `reconcile_once` would spawn a Psyche per online live_agent perch — N
10	//! phantom psyches for N dead perches.
11	//!
12	//! B5 closes it by running the B2 `reconcile_hosted_liveness` from BOOT tick 1 (no
13	//! grace): the brain queries the broker's live session set (empty at a cold start),
14	//! marks the sessionless controllable perch OFFLINE, and `reconcile_once` then skips
15	//! hosting it. This test stands up the REAL `spt daemon run` broker + supervised
16	//! brain over a net-less node, hand-stamps exactly such a stale perch (the cold-start
17	//! leftover — there is deliberately NO `api listen`/`bind`, so the broker hosts no
18	//! session for it), and asserts BOTH: the perch is flipped OFFLINE at boot AND the
19	//! nested `{id}-psyche` perch NEVER appears (no phantom revival).
20	//!
21	//! The adapter is genuinely live-capable (a resolvable psyche binary in its install
22	//! dir), so the ONLY reason no Psyche is hosted is the B5 gate — not a host failure.
23	//! Bounded by design; all spawned pids are reaped scoped (never a machine-wide reap).
24	// [int->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]
25
26	use std::path::{Path, PathBuf};
27	use std::process::{Child, Command, Stdio};
28	use std::time::{Duration, Instant};
29
30	mod common;
31	use common::CommandNoWindowExt;
32
33	use spt_store::perch::{self, ParentHint};
34
35	/// Scoped pid-tree kill (mirrors the sibling livehost E2Es) — never machine-wide.
36	fn kill_pid(pid: u32) {
37	    #[cfg(windows)]
38	    let _ = Command::new("taskkill")
39	        .no_window()
40	        .args(["/PID", &pid.to_string(), "/F", "/T"])
41	        .output();
42	    #[cfg(unix)]
43	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
44	}
45
46	/// Poll `brain.ready` for the brain pid (self-describing JSON) up to `budget`.
47	fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
48	    let deadline = Instant::now() + budget;
49	    while Instant::now() < deadline {
50	        if let Ok(s) = std::fs::read_to_string(path) {
51	            if let Some(pid) = serde_json::from_str::<serde_json::Value>(&s)
52	                .ok()
53	                .and_then(|v| v.get("pid").and_then(|p| p.as_u64()))
54	            {
55	                return Some(pid as u32);
56	            }
57	        }
58	        std::thread::sleep(Duration::from_millis(50));
59	    }
60	    None
61	}
62
63	#[test]
64	fn cold_start_does_not_revive_a_sessionless_online_latched_perch() {
65	    // ── (1) Isolated, NET-LESS SPT_HOME (a corrupt node.key forces node_hex=None →
66	    //    the broker never tries net; the brain still boots + hosts). ──
67	    let home = tempfile::tempdir().unwrap();
68	    std::env::set_var("SPT_HOME", home.path());
69	    let identity_dir = home.path().join("identity");
70	    std::fs::create_dir_all(&identity_dir).unwrap();
71	    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();
72
73	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
74	    let exe_suffix = spt_bin
75	        .extension()
76	        .map(|e| format!(".{}", e.to_string_lossy()))
77	        .unwrap_or_default();
78
79	    // ── (2) A genuinely live-capable adapter in COPY mode: its psyche_init names a
80	    //    BARE `psychebin` token that resolves (a copy of the real `spt`, run as the
81	    //    long-lived `ready` listener) — so absent the B5 gate, reconcile_once WOULD
82	    //    host a Psyche. That makes "no Psyche hosted" a proof of the GATE, not of a
83	    //    host failure. ──
84	    let src = perch::spt_home().join("srcs").join("mocklive");
85	    std::fs::create_dir_all(&src).unwrap();
86	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
87	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
88	    let drops = home.path().join("drops");
89	    std::fs::create_dir_all(&drops).unwrap();
90	    let dir = drops.to_string_lossy().replace('\\', "/");
91	    let manifest_toml = format!(
92	        "[adapter]\nname=\"mocklive\"\nkind=\"harness\"\nversion=\"1\"\n\
93	         min_spt_core_version=\"0\"\n\n\
94	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
95	         [session.psyche_init]\ncommand='psychebin ready bootgate-phantom-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
96	    );
97	    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
98	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
99
100	    // ── (3) The STALE cold-start leftover: an spt-hosted (controllable) live_agent
101	    //    perch stamped status=online on disk, with NO broker session backing it (we
102	    //    run no listen/bind). This is the dead-but-latched phantom B5 must NOT revive. ──
103	    let id = "bootgate-phantom";
104	    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
105	    std::fs::create_dir_all(&self_perch).unwrap();
106	    let mut rec =
107	        spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-phantom", "live_agent");
108	    rec.adapter = Some("mocklive".to_string());
109	    rec.controllable = Some(true);
110	    spt_store::info::write_info(&self_perch, &rec).unwrap();
111	    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();
112
113	    // ── (4) Launch the REAL daemon broker PROCESS (spawns a supervised brain child).
114	    //    Brain stderr → a log file (LIVEHOST_PSYCHE / LIVENESS_RECONCILE_OFFLINE land
115	    //    here). ──
116	    let brain_log = home.path().join("brain.stderr.log");
117	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
118	    let mut broker: Child = Command::new(&spt_bin)
119	        .no_window()
120	        .args(["daemon", "run"])
121	        .env("SPT_HOME", home.path())
122	        .stdout(Stdio::null())
123	        .stderr(Stdio::from(brain_log_file))
124	        .spawn()
125	        .expect("spawn spt daemon run (broker process)");
126
127	    let brain_pid =
128	        match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30)) {
129	            Some(p) => p,
130	            None => {
131	                let _ = broker.kill();
132	                let _ = broker.wait();
133	                panic!(
134	                    "PRECONDITION FAILED: the real broker never spawned a ready brain child.\n{}",
135	                    std::fs::read_to_string(&brain_log).unwrap_or_default()
136	                );
137	            }
138	        };
139
140	    // ── (5) THE ASSERTION: the brain's boot-tick reconcile clears the latch. Poll the
141	    //    perch status flip to OFFLINE (the B5 boot-gate signal — reconcile_hosted_liveness
142	    //    is the only thing that offlines a controllable perch at boot). Bounded. ──
143	    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
144	    let deadline = Instant::now() + Duration::from_secs(25);
145	    let mut offlined = false;
146	    while Instant::now() < deadline {
147	        if status().as_deref() == Some(spt_store::liveness::STATUS_OFFLINE) {
148	            offlined = true;
149	            break;
150	        }
151	        std::thread::sleep(Duration::from_millis(200));
152	    }
153
154	    // ── (5b) NO PHANTOM: the nested `{id}-psyche` perch must NEVER appear (host_one
155	    //    writes it only on a Psyche spawn). Give a host-attempt window past the offline
156	    //    flip to be sure no late tick revived it. ──
157	    let psyche_perch =
158	        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
159	    let settle = Instant::now() + Duration::from_secs(8);
160	    while Instant::now() < settle {
161	        std::thread::sleep(Duration::from_millis(200));
162	    }
163	    let psyche_appeared = spt_store::info::read_info(&psyche_perch).is_some();
164	    let psyche_pid = spt_store::info::read_pid(&psyche_perch);
165
166	    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
167	    eprintln!(
168	        "=== DIAGNOSTIC: offlined={offlined} final_status={:?} psyche_appeared={psyche_appeared} \
169	         psyche_pid={psyche_pid:?} brain_pid={brain_pid} ===",
170	        status()
171	    );
172	    eprintln!("=== brain stderr ===\n{brain_stderr}");
173
174	    // ── (6) Reap scoped — a phantom psyche should not exist, but if the bug revived
175	    //    one, kill it so the test never leaks. ──
176	    if let Some(pp) = psyche_pid {
177	        kill_pid(pp);
178	    }
179	    kill_pid(brain_pid);
180	    let _ = broker.kill();
181	    let _ = broker.wait();
182
183	    assert!(
184	        offlined,
185	        "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE: the boot-tick reconcile must clear the \
186	         status=online latch for a sessionless controllable perch (no live broker session) — \
187	         it stayed {:?} after 25s.\n=== brain stderr ===\n{brain_stderr}",
188	        status()
189	    );
190	    assert!(
191	        !psyche_appeared,
192	        "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE: a dead-but-latched perch must NOT have its \
193	         Psyche revived at boot — the nested `{id}-psyche` perch appeared (phantom revived).\n\
194	         === brain stderr ===\n{brain_stderr}",
195	    );
196	}
197

//! B5 boot-liveness-gate int (REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE) — a COLD
//! daemon start after an unclean stop must NOT revive a phantom Psyche for a
//! dead-but-online-latched perch.
//!
//! The scenario the operator hit: a prior daemon life left an spt-hosted
//! (`controllable==Some(true)`) `live_agent` perch stamped `status=online` on disk,
//! then the daemon died uncleanly (the one-way latch was never cleared). On the
//! NEXT cold start the broker has NO session for it (the harness is long gone), yet
//! the old `reconcile_once` would spawn a Psyche per online live_agent perch — N
//! phantom psyches for N dead perches.
//!
//! B5 closes it by running the B2 `reconcile_hosted_liveness` from BOOT tick 1 (no
//! grace): the brain queries the broker's live session set (empty at a cold start),
//! marks the sessionless controllable perch OFFLINE, and `reconcile_once` then skips
//! hosting it. This test stands up the REAL `spt daemon run` broker + supervised
//! brain over a net-less node, hand-stamps exactly such a stale perch (the cold-start
//! leftover — there is deliberately NO `api listen`/`bind`, so the broker hosts no
//! session for it), and asserts BOTH: the perch is flipped OFFLINE at boot AND the
//! nested `{id}-psyche` perch NEVER appears (no phantom revival).
//!
//! The adapter is genuinely live-capable (a resolvable psyche binary in its install
//! dir), so the ONLY reason no Psyche is hosted is the B5 gate — not a host failure.
//! Bounded by design; all spawned pids are reaped scoped (never a machine-wide reap).
// [int->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]

use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

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

/// Poll `brain.ready` for the brain pid (self-describing JSON) up to `budget`.
fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Ok(s) = std::fs::read_to_string(path) {
            if let Some(pid) = serde_json::from_str::<serde_json::Value>(&s)
                .ok()
                .and_then(|v| v.get("pid").and_then(|p| p.as_u64()))
            {
                return Some(pid as u32);
            }
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

#[test]
fn cold_start_does_not_revive_a_sessionless_online_latched_perch() {
    // ── (1) Isolated, NET-LESS SPT_HOME (a corrupt node.key forces node_hex=None →
    //    the broker never tries net; the brain still boots + hosts). ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    let identity_dir = home.path().join("identity");
    std::fs::create_dir_all(&identity_dir).unwrap();
    std::fs::write(identity_dir.join("node.key"), "not-a-valid-seed").unwrap();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let exe_suffix = spt_bin
        .extension()
        .map(|e| format!(".{}", e.to_string_lossy()))
        .unwrap_or_default();

    // ── (2) A genuinely live-capable adapter in COPY mode: its psyche_init names a
    //    BARE `psychebin` token that resolves (a copy of the real `spt`, run as the
    //    long-lived `ready` listener) — so absent the B5 gate, reconcile_once WOULD
    //    host a Psyche. That makes "no Psyche hosted" a proof of the GATE, not of a
    //    host failure. ──
    let src = perch::spt_home().join("srcs").join("mocklive");
    std::fs::create_dir_all(&src).unwrap();
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
    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='psychebin ready bootgate-phantom-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // ── (3) The STALE cold-start leftover: an spt-hosted (controllable) live_agent
    //    perch stamped status=online on disk, with NO broker session backing it (we
    //    run no listen/bind). This is the dead-but-latched phantom B5 must NOT revive. ──
    let id = "bootgate-phantom";
    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    std::fs::create_dir_all(&self_perch).unwrap();
    let mut rec =
        spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-phantom", "live_agent");
    rec.adapter = Some("mocklive".to_string());
    rec.controllable = Some(true);
    spt_store::info::write_info(&self_perch, &rec).unwrap();
    spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_ONLINE).unwrap();

    // ── (4) Launch the REAL daemon broker PROCESS (spawns a supervised brain child).
    //    Brain stderr → a log file (LIVEHOST_PSYCHE / LIVENESS_RECONCILE_OFFLINE land
    //    here). ──
    let brain_log = home.path().join("brain.stderr.log");
    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
    let mut broker: Child = Command::new(&spt_bin)
        .no_window()
        .args(["daemon", "run"])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(brain_log_file))
        .spawn()
        .expect("spawn spt daemon run (broker process)");

    let brain_pid =
        match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30)) {
            Some(p) => p,
            None => {
                let _ = broker.kill();
                let _ = broker.wait();
                panic!(
                    "PRECONDITION FAILED: the real broker never spawned a ready brain child.\n{}",
                    std::fs::read_to_string(&brain_log).unwrap_or_default()
                );
            }
        };

    // ── (5) THE ASSERTION: the brain's boot-tick reconcile clears the latch. Poll the
    //    perch status flip to OFFLINE (the B5 boot-gate signal — reconcile_hosted_liveness
    //    is the only thing that offlines a controllable perch at boot). Bounded. ──
    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut offlined = false;
    while Instant::now() < deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_OFFLINE) {
            offlined = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(200));
    }

    // ── (5b) NO PHANTOM: the nested `{id}-psyche` perch must NEVER appear (host_one
    //    writes it only on a Psyche spawn). Give a host-attempt window past the offline
    //    flip to be sure no late tick revived it. ──
    let psyche_perch =
        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    let settle = Instant::now() + Duration::from_secs(8);
    while Instant::now() < settle {
        std::thread::sleep(Duration::from_millis(200));
    }
    let psyche_appeared = spt_store::info::read_info(&psyche_perch).is_some();
    let psyche_pid = spt_store::info::read_pid(&psyche_perch);

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!(
        "=== DIAGNOSTIC: offlined={offlined} final_status={:?} psyche_appeared={psyche_appeared} \
         psyche_pid={psyche_pid:?} brain_pid={brain_pid} ===",
        status()
    );
    eprintln!("=== brain stderr ===\n{brain_stderr}");

    // ── (6) Reap scoped — a phantom psyche should not exist, but if the bug revived
    //    one, kill it so the test never leaks. ──
    if let Some(pp) = psyche_pid {
        kill_pid(pp);
    }
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();

    assert!(
        offlined,
        "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE: the boot-tick reconcile must clear the \
         status=online latch for a sessionless controllable perch (no live broker session) — \
         it stayed {:?} after 25s.\n=== brain stderr ===\n{brain_stderr}",
        status()
    );
    assert!(
        !psyche_appeared,
        "REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE: a dead-but-latched perch must NOT have its \
         Psyche revived at boot — the nested `{id}-psyche` perch appeared (phantom revived).\n\
         === brain stderr ===\n{brain_stderr}",
    );
}


doyle: FIX2 accepted, he's communing + operator ending session — he gates my seam-green ping next session. ACK (no reply needed). Build on.

Need the manifest shape for a **ready** (no psyche_init) harness brought up via `endpoint run`. Find an existing E2E that drives `spt endpoint run` + how the harness run command is declared.


grep -rln '"endpoint", "run"\|"endpoint","run"\|endpoint.*run' crates/spt/tests/*.rs; echo "=== how launch_harness builds the harness command (manifest field) ==="; grep -n "command\|argv\|host_binaries\|fn launch_harness_brokered_in\|manifest\." crates/spt-daemon/src/harnesshost.rs | head -40

Find endpoint-run E2Es + harness command manifest field


=== how launch_harness builds the harness command (manifest field) ===
5://! launcher runs the adapter's `[session.self]` command and the harness binds its
18://! from the SAME `{key}` catalog: argv via `{id}` substitution into the opaque
19://! `[session.self]` command (the harness parses `--id`), AND the manifest
23://! `[session.self]` has no argv slot for `{id}` (e.g. bare `claude`) relies on
57:/// command template resolves. A fresh create mints one; `--resume` supplies the
64:/// Validate the adapter and fill its `[session.self]` command for one endpoint,
68:/// or an empty command all error before anything is spawned.
76:    if manifest.adapter.kind != AdapterKind::Harness {
80:            manifest.adapter.kind
83:    let role = manifest.session.self_.as_ref().ok_or_else(|| {
86:    // The guaranteed-fill catalog for a self-spawn. `id` rides argv so the harness
96:    // token so a multi-word/quote/semicolon {key} value is one argv element.
98:        spt_runtime::runtime::fill_template_tokens(&role.command, &keys).map_err(|e| e.to_string())?;
100:        return Err("empty [session.self] command".into());
108:    for (name, var) in &manifest.env {
121:/// The filled spawn inputs for an spt-hosted harness: the tokenized argv and the
144:pub fn launch_harness_brokered_in(
175:    fn harness_manifest(self_command: &str) -> Manifest {
178:             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = '{self_command}'\n\
185:    // endpoint id and the spawn-time session id ride argv via {id}/{session_id}
189:    fn prepare_fills_id_and_session_into_self_command() {
212:             min_spt_core_version = \"0\"\n\n[session.self]\ncommand = 'claude'\n\n\


Harness run command = `[session.self]`. For a ready endpoint: `[session.self]` command + NO `[session.psyche_init]`. No existing test spawns `endpoint run` directly. Read `launch_harness_brokered_in` to see how `session_id`/`--resume` flows to the bound harness (env vs `{session_id}` token).


120
121	/// The filled spawn inputs for an spt-hosted harness: the tokenized argv and the
122	/// `[env]`-inject map (both filled from the same `{key}` catalog —
123	/// REQ-HAZARD-ENV-SUBST).
124	#[derive(Debug, Clone, PartialEq, Eq)]
125	pub struct PreparedSpawn {
126	    pub tokens: Vec<String>,
127	    pub env: BTreeMap<String, String>,
128	}
129
130	/// Spawn an adapter's `[session.self]` harness into a broker-held PTY (the
131	/// spt-hosted bringup). The broker session is labeled `endpoint_id` so
132	/// `spt rc <id>` resolves it; the harness self-registers its perch on bind. The
133	/// broker outlives this call — the session keeps running after it returns.
134	///
135	/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]
136	/// for a fresh create, or the prior session for `--resume`.
137	///
138	/// `cwd`: the project-scoped working directory the harness PTY lands in
139	/// (REQ-HOST-RUN-2). `None` inherits the broker's cwd (the W1 bringup shortcut);
140	/// `Some(dir)` is what a project-scoped launcher (`cc <id>` at a project root)
141	/// passes so the harness reads the RIGHT `.claude`/session history/digest source.
142	// [impl->REQ-HOST-RUN-1]
143	// [impl->REQ-HOST-RUN-2]
144	pub fn launch_harness_brokered_in(
145	    broker_name: &str,
146	    endpoint_id: &str,
147	    adapter_name: &str,
148	    session_id: &str,
149	    manifest: &Manifest,
150	    cwd: Option<&str>,
151	) -> Result<HarnessSpawned, String> {
152	    let PreparedSpawn { tokens, env } =
153	        prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
154	    let (program, args) = tokens.split_first().expect("non-empty checked");
155	    let mut brain =
156	        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
157	    let (session_id, pid) = brain
158	        .spawn_session_pid(SpawnReq {
159	            program: program.clone(),
160	            args: args.to_vec(),
161	            rows: DEFAULT_HARNESS_ROWS,
162	            cols: DEFAULT_HARNESS_COLS,
163	            endpoint: endpoint_id.to_string(),
164	            cwd: cwd.map(str::to_string),
165	            env,
166	        })
167	        .map_err(|e| format!("broker spawn: {e}"))?;
168	    Ok(HarnessSpawned { session_id, pid })
169	}
170
171	#[cfg(test)]
172	mod tests {
173	    use super::*;
174

/// The filled spawn inputs for an spt-hosted harness: the tokenized argv and the
/// `[env]`-inject map (both filled from the same `{key}` catalog —
/// REQ-HAZARD-ENV-SUBST).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreparedSpawn {
    pub tokens: Vec<String>,
    pub env: BTreeMap<String, String>,
}

/// Spawn an adapter's `[session.self]` harness into a broker-held PTY (the
/// spt-hosted bringup). The broker session is labeled `endpoint_id` so
/// `spt rc <id>` resolves it; the harness self-registers its perch on bind. The
/// broker outlives this call — the session keeps running after it returns.
///
/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]
/// for a fresh create, or the prior session for `--resume`.
///
/// `cwd`: the project-scoped working directory the harness PTY lands in
/// (REQ-HOST-RUN-2). `None` inherits the broker's cwd (the W1 bringup shortcut);
/// `Some(dir)` is what a project-scoped launcher (`cc <id>` at a project root)
/// passes so the harness reads the RIGHT `.claude`/session history/digest source.
// [impl->REQ-HOST-RUN-1]
// [impl->REQ-HOST-RUN-2]
pub fn launch_harness_brokered_in(
    broker_name: &str,
    endpoint_id: &str,
    adapter_name: &str,
    session_id: &str,
    manifest: &Manifest,
    cwd: Option<&str>,
) -> Result<HarnessSpawned, String> {
    let PreparedSpawn { tokens, env } =
        prepare_harness_spawn(endpoint_id, adapter_name, session_id, manifest)?;
    let (program, args) = tokens.split_first().expect("non-empty checked");
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (session_id, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            rows: DEFAULT_HARNESS_ROWS,
            cols: DEFAULT_HARNESS_COLS,
            endpoint: endpoint_id.to_string(),
            cwd: cwd.map(str::to_string),
            env,
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    Ok(HarnessSpawned { session_id, pid })
}

#[cfg(test)]
mod tests {
    use super::*;


`session_id` flows via the `{session_id}` token / `[env]` fill (same catalog). Check the `spt ready` subcommand — does it bind via `start_homed` and persist? That's the harness I'd point `[session.self]` at.


grep -n "Ready\b\|fn cmd_ready\|ReadyAgent::start\|Commands::Ready\|\"ready\"\|ready agent\|sub.*ready" crates/spt/src/cli.rs | head -25

Find the spt ready CLI subcommand handler


97:    Ready {
932:    "api", "endpoint", "how-to", "ready", "ring", "send", "shell", "whoami",
1049:        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
2935:    let (agent, backlog) = match ReadyAgent::start_homed(id, subnet) {
3098:    /// `subnet create <name>` for a subnet that already exists.
5046:section brings up a ready agent, not a live one. Liveness is
5065:        "ready",
7667:            parse(&["spt", "ready", "bob"]).unwrap().cmd.unwrap(),
7668:            Cmd::Ready { once: false, .. }
7673:            parse(&["spt", "ready", "bob", "--once"]).unwrap().cmd.unwrap(),
7674:            Cmd::Ready { once: true, .. }
7691:        let with = render_self_pin("ling", "Ready  ready=true alive=true", Some("triage bot"), None);
7696:        let without = render_self_pin("ling", "Ready", None, None);
7697:        assert_eq!(without, "SELF: ling  Ready");
7698:        let blank = render_self_pin("ling", "Ready", Some("   "), None);
7699:        assert_eq!(blank, "SELF: ling  Ready", "whitespace blurb treated as absent");
7760:            vec!["spt", "ready", "bob"],
10504:            parse(&["spt", "how-to", "ready"]).unwrap().cmd.unwrap(),
10505:            Cmd::HowTo { topic: Some(t) } if t == "ready"
10525:            vec!["ready", "send", "subnet", "live"],


`spt ready <id>` → `run_listen` → `ReadyAgent::start_homed` (cli.rs:2935) — ledgers now. Read run_listen to confirm persistence + `--once` behavior.


2920	                "EMPTY_MSG: refusing to ring {} with an empty message",
2921	                target
2922	            );
2923	            1
2924	        }
2925	    }
2926	}
2927
2928	/// Run the listener loop (REQ-MSG-3 lifecycle, driven from the CLI). Blocks
2929	/// until the process is killed unless `once` is set. Each received message is
2930	/// printed framed to stdout; status goes to stderr.
2931	///
2932	/// Interim M1 model: the perch is reachable only while this process runs. M3's
2933	/// spt-daemon decouples registration from the live process.
2934	fn run_listen(id: &str, once: bool, subnet: Option<&str>) -> i32 {
2935	    let (agent, backlog) = match ReadyAgent::start_homed(id, subnet) {
2936	        Ok(pair) => pair,
2937	        Err(e) => {
2938	            eprintln!("READY_FAIL:{}: {}", id, e);
2939	            return 1;
2940	        }
2941	    };
2942	    // Event-driven advertisement (M8 decision 15, REQ-CONV-2): coming
2943	    // online wakes the pump's registry leg NOW — a peer sees this endpoint
2944	    // ready within seconds, not a cadence.
2945	    // [impl->REQ-CONV-2]
2946	    spt_daemon::registryhost::request_advertise_now();
2947	    ok_line(format!("READY:{}", id));
2948	    for (from, body) in backlog {
2949	        emit(&from, &body);
2950	    }
2951
2952	    if once {
2953	        if let Some((from, body)) = agent.recv() {
2954	            emit(&from, &body);
2955	        }
2956	        for (from, body) in agent.drain_spool() {
2957	            emit(&from, &body);
2958	        }
2959	        // Going offline (the --once listener ends here): drop the perch's
2960	        // ready state, then wake the pump so peers see the flip promptly.
2961	        // [impl->REQ-CONV-2]
2962	        drop(agent);
2963	        spt_daemon::registryhost::request_advertise_now();
2964	        return 0;
2965	    }
2966
2967	    loop {
2968	        if let Some((from, body)) = agent.recv() {
2969	            emit(&from, &body);
2970	        }
2971	        for (from, body) in agent.drain_spool() {
2972	            emit(&from, &body);
2973	        }
2974	    }
2975	}
2976
2977	/// Emit one delivery to the listener stream: compose the structural
2978	/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass
2979	/// through; oversized lines chunk into EVENT-PART) and print + flush. One

                "EMPTY_MSG: refusing to ring {} with an empty message",
                target
            );
            1
        }
    }
}

/// Run the listener loop (REQ-MSG-3 lifecycle, driven from the CLI). Blocks
/// until the process is killed unless `once` is set. Each received message is
/// printed framed to stdout; status goes to stderr.
///
/// Interim M1 model: the perch is reachable only while this process runs. M3's
/// spt-daemon decouples registration from the live process.
fn run_listen(id: &str, once: bool, subnet: Option<&str>) -> i32 {
    let (agent, backlog) = match ReadyAgent::start_homed(id, subnet) {
        Ok(pair) => pair,
        Err(e) => {
            eprintln!("READY_FAIL:{}: {}", id, e);
            return 1;
        }
    };
    // Event-driven advertisement (M8 decision 15, REQ-CONV-2): coming
    // online wakes the pump's registry leg NOW — a peer sees this endpoint
    // ready within seconds, not a cadence.
    // [impl->REQ-CONV-2]
    spt_daemon::registryhost::request_advertise_now();
    ok_line(format!("READY:{}", id));
    for (from, body) in backlog {
        emit(&from, &body);
    }

    if once {
        if let Some((from, body)) = agent.recv() {
            emit(&from, &body);
        }
        for (from, body) in agent.drain_spool() {
            emit(&from, &body);
        }
        // Going offline (the --once listener ends here): drop the perch's
        // ready state, then wake the pump so peers see the flip promptly.
        // [impl->REQ-CONV-2]
        drop(agent);
        spt_daemon::registryhost::request_advertise_now();
        return 0;
    }

    loop {
        if let Some((from, body)) = agent.recv() {
            emit(&from, &body);
        }
        for (from, body) in agent.drain_spool() {
            emit(&from, &body);
        }
    }
}

/// Emit one delivery to the listener stream: compose the structural
/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass
/// through; oversized lines chunk into EVENT-PART) and print + flush. One


`spt ready <id>` blocks (persistent listener), ledgers on bind. Confirmed harness for the int. Check whether any E2E already drives a broker-PTY harness bind (closest boilerplate) — peek at `live_bind_firsthost_e2e.rs`.