/// Author the **Self-resume commune** at a boundary: stamp `Source: self-commune`
/// provenance on each slice and route it (precedence-guarded, two-tier) into
/// the tracked context store for the re-entry (T7) to preload. The Self
/// authored it, so each write is an llm write. Self communes are the strong
/// **live** signal (commune-source asymmetry), but the routing still honors the
/// tags — a Self that splits its slices gets the split.
// [impl->REQ-INST-2]
pub fn write_resume_commune(
    id: &str,
    project_id: &str,
    body: &str,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<TierWrite>> {
    let slices = parse_two_slice(body);
    let live = slices.live.map(|s| stamp_provenance(SOURCE_SELF_COMMUNE, &s));
    let project = slices.project.map(|s| stamp_provenance(SOURCE_SELF_COMMUNE, &s));
    route_slices(id, project_id, live.as_deref(), project.as_deref(), now_ms, window_ms)
}

//! signoff + boundary lifecycle (M2b T6) — the load-bearing ordering invariants.
//!
//! These are the sister project's most expensive bugs; we **copy the ordering**
//! and clean-room the wiring (the concrete echo/compose/deliver effects are the
//! caller's — T8). Three invariants:
//!
//! - **3.3 echo-before-signoff** ([`graceful_signoff`]): synchronously run the
//!   echo-commune (final context delta) *before* composing INIT_SIGNOFF; skip
//!   the echo only when `session_id` is missing.
//! - **1.1 grace-before-signoff** ([`graceful_signoff`]): the grace wait
//!   completes and the recheck binds `still_gone` *before* any signoff write —
//!   a Self that transiently recovered (binary handoff, brief stale poll) is
//!   **not** torn down.
//! - **3.2 stale-signoff sweep** ([`sweep_stale_signoff`]): on every listener
//!   start, sweep a leftover `<id>-signoff.md` so a prior generation's sentinel
//!   never reads as a live signoff and kills a fresh start.
//!
//! Plus the **boundary Self-resume commune** ([`write_resume_commune`]): M2a's
//! `api boundary` rebinds the rotated session; M2b authors the resume commune
//! the re-entry (T7) preloads.
//
// M2b implements the GRACEFUL teardown paths (spt shutdown / spt suspend /
// boundary). Orphan *crash* detection after an unexpected harness death needs
// the always-on supervisor — that orphan-watch is M3, and it reuses this exact
// ordering. Copy the ordering, not the CC-coupled wiring.
// [impl->REQ-HAZARD-ECHO-BEFORE-SIGNOFF]
// [impl->REQ-HAZARD-GRACE-BEFORE-SIGNOFF]
// [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]

use crate::echo::stamp_provenance;
use crate::ingest::{route_slices, TierWrite};
use spt_proto::envelope::parse_two_slice;
use std::path::Path;
use std::time::Duration;

/// Provenance tag for a Self-authored boundary commune (strong live signal —
/// `docs/CONTEXT-MEMORY.md` commune-source asymmetry).
pub const SOURCE_SELF_COMMUNE: &str = "self-commune";

/// Outcome of a graceful signoff attempt.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SignoffOutcome {
    /// Whether the echo-commune ran (false ⇒ skipped: no session_id).
    pub echo_ran: bool,
    /// Whether the signoff was composed+delivered (false ⇒ the Self recovered
    /// across the grace wait and was spared).
    pub signed_off: bool,
}

/// Run the graceful signoff sequence with the load-bearing ordering baked in.
///
/// The concrete effects are injected so the invariant is testable independent of
/// the echo/compose wiring:
/// - `echo` — run the final-delta echo-commune (3.3);
/// - `recheck_still_gone` — re-evaluate liveness *after* the grace wait (1.1);
/// - `compose_deliver` — compose + deliver INIT_SIGNOFF.
///
/// Order is fixed: `echo` (if `session_id`) → grace wait → `recheck_still_gone`
/// → `compose_deliver` (only if still gone). The recheck binds `still_gone`
/// before any signoff write; the echo precedes the signoff compose.
pub fn graceful_signoff<E>(
    session_id: Option<&str>,
    grace: Duration,
    echo: impl FnOnce() -> Result<(), E>,
    recheck_still_gone: impl FnOnce() -> bool,
    compose_deliver: impl FnOnce() -> Result<(), E>,
) -> Result<SignoffOutcome, E> {
    // 3.3 — echo (final context delta) BEFORE any signoff compose; skip only on
    // a missing session_id.
    let echo_ran = if session_id.is_some() {
        echo()?;
        true
    } else {
        false
    };

    // 1.1 — grace wait, THEN bind still_gone, BEFORE any signoff write.
    if !grace.is_zero() {
        std::thread::sleep(grace);
    }
    let still_gone = recheck_still_gone();

    let signed_off = if still_gone {
        compose_deliver()?;
        true
    } else {
        // The Self recovered across the grace window — never sign off a live Self.
        false
    };

    Ok(SignoffOutcome {
        echo_ran,
        signed_off,
    })
}

/// Sweep a stale `<id>-signoff.md` from `signoff_dir` on listener start (3.2).
/// Returns whether one was removed. Signoff files are write-once per
/// generation; a leftover from a prior session must never read as a live signoff.
pub fn sweep_stale_signoff(signoff_dir: &Path, id: &str) -> std::io::Result<bool> {
    let stale = signoff_dir.join(format!("{id}-signoff.md"));
    match std::fs::remove_file(&stale) {
        Ok(()) => {
            eprintln!("SIGNOFF_SWEEP:{id} (removed stale sentinel)");
            Ok(true)
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
        Err(e) => Err(e),
    }
}

/// Author the **Self-resume commune** at a boundary: stamp `Source: self-commune`
/// provenance and write it (precedence-guarded) into the perch's context store
/// for the re-entry (T7) to preload. The Self authored it, so it is an llm write.
pub fn write_resume_commune(
    id: &str,
    hint: ParentHint<'_>,
    body: &str,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<WriteOutcome> {
    let ctx = context_path(id, hint);
    if let Some(parent) = ctx.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let content = stamp_provenance(SOURCE_SELF_COMMUNE, body);
    write_context(&ctx, &content, WriteSource::Llm, now_ms, window_ms)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::context::DEFAULT_PROTECTION_WINDOW_MS;
    use crate::test_support::with_home;
    use std::cell::RefCell;

    #[test] // [unit->REQ-HAZARD-ECHO-BEFORE-SIGNOFF] echo runs BEFORE the signoff compose.
    fn echo_precedes_signoff_compose() {
        let log: RefCell<Vec<&str>> = RefCell::new(Vec::new());
        let out = graceful_signoff::<()>(
            Some("sid-1"),
            Duration::ZERO,
            || {
                log.borrow_mut().push("echo");
                Ok(())
            },
            || {
                log.borrow_mut().push("recheck");
                true
            },
            || {
                log.borrow_mut().push("compose");
                Ok(())
            },
        )
        .unwrap();
        assert_eq!(out, SignoffOutcome { echo_ran: true, signed_off: true });
        // Ordering: echo and recheck both strictly precede compose.
        assert_eq!(*log.borrow(), ["echo", "recheck", "compose"]);
    }

    #[test] // [unit->REQ-HAZARD-GRACE-BEFORE-SIGNOFF] a recovered Self is NOT signed off.
    fn recovered_self_is_not_signed_off() {
        let composed = RefCell::new(false);
        let out = graceful_signoff::<()>(
            Some("sid-1"),
            Duration::ZERO,
            || Ok(()),
            || false, // recheck: the Self came back alive
            || {
                *composed.borrow_mut() = true;
                Ok(())
            },
        )
        .unwrap();
        assert_eq!(out, SignoffOutcome { echo_ran: true, signed_off: false });
        assert!(!*composed.borrow(), "must not compose signoff for a live Self");
    }

    #[test] // [unit->REQ-HAZARD-ECHO-BEFORE-SIGNOFF] a missing session_id skips the echo.
    fn missing_session_skips_echo() {
        let echoed = RefCell::new(false);
        let out = graceful_signoff::<()>(
            None,
            Duration::ZERO,
            || {
                *echoed.borrow_mut() = true;
                Ok(())
            },
            || true,
            || Ok(()),
        )
        .unwrap();
        assert!(!out.echo_ran && !*echoed.borrow(), "no echo without a session_id");
        assert!(out.signed_off, "signoff still proceeds when still gone");
    }

    #[test] // [unit->REQ-HAZARD-STALE-SIGNOFF-SENTINEL] a stale signoff sentinel is swept on start.
    fn stale_signoff_swept() {
        let dir = tempfile::tempdir().unwrap();
        let stale = dir.path().join("doyle-signoff.md");
        std::fs::write(&stale, "old signoff").unwrap();

        assert!(sweep_stale_signoff(dir.path(), "doyle").unwrap(), "should sweep one");
        assert!(!stale.exists(), "stale sentinel must be gone");
        // A fresh start with none present sweeps nothing, no error.
        assert!(!sweep_stale_signoff(dir.path(), "doyle").unwrap());
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] boundary writes a provenance-tagged resume commune.
    fn boundary_writes_resume_commune() {
        with_home(|_| {
            let o = write_resume_commune(
                "doyle",
                ParentHint::Infer,
                "where I left off",
                1000,
                DEFAULT_PROTECTION_WINDOW_MS,
            )
            .unwrap();
            assert!(matches!(o, WriteOutcome::Written { .. }));
            let ctx = context_path("doyle", ParentHint::Infer);
            let written = std::fs::read_to_string(&ctx).unwrap();
            assert!(written.contains("Source: self-commune"), "provenance: {written:?}");
            assert!(written.contains("where I left off"));
        });
    }
}

-/// provenance and write it (precedence-guarded) into the perch's context store

-/// for the re-entry (T7) to preload. The Self authored it, so it is an llm write.

+/// provenance on each slice and route it (precedence-guarded, two-tier) into

+/// the tracked context store for the re-entry (T7) to preload. The Self

+/// authored it, so each write is an llm write. Self communes are the strong

+/// **live** signal (commune-source asymmetry), but the routing still honors the

+/// tags — a Self that splits its slices gets the split.

-    if let Some(parent) = ctx.parent() {

-        std::fs::create_dir_all(parent)?;

-    let content = stamp_provenance(SOURCE_SELF_COMMUNE, body);

-    write_context(&ctx, &content, WriteSource::Llm, now_ms, window_ms)

+    let slices = parse_two_slice(body);

+    let live = slices.live.map(|s| stamp_provenance(SOURCE_SELF_COMMUNE, &s));

+    let project = slices.project.map(|s| stamp_provenance(SOURCE_SELF_COMMUNE, &s));

+    route_slices(id, project_id, live.as_deref(), project.as_deref(), now_ms, window_ms)


Fix the signoff test that used `context_path`:


200	    #[test] // [unit->REQ-HAZARD-STALE-SIGNOFF-SENTINEL] a stale signoff sentinel is swept on start.
201	    fn stale_signoff_swept() {
202	        let dir = tempfile::tempdir().unwrap();
203	        let stale = dir.path().join("doyle-signoff.md");
204	        std::fs::write(&stale, "old signoff").unwrap();
205
206	        assert!(sweep_stale_signoff(dir.path(), "doyle").unwrap(), "should sweep one");
207	        assert!(!stale.exists(), "stale sentinel must be gone");
208	        // A fresh start with none present sweeps nothing, no error.
209	        assert!(!sweep_stale_signoff(dir.path(), "doyle").unwrap());
210	    }
211
212	    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] boundary writes a provenance-tagged resume commune.
213	    fn boundary_writes_resume_commune() {
214	        with_home(|_| {
215	            let o = write_resume_commune(
216	                "doyle",
217	                ParentHint::Infer,
218	                "where I left off",
219	                1000,
220	                DEFAULT_PROTECTION_WINDOW_MS,
221	            )
222	            .unwrap();
223	            assert!(matches!(o, WriteOutcome::Written { .. }));
224	            let ctx = context_path("doyle", ParentHint::Infer);
225	            let written = std::fs::read_to_string(&ctx).unwrap();
226	            assert!(written.contains("Source: self-commune"), "provenance: {written:?}");
227	            assert!(written.contains("where I left off"));
228	        });
229	    }
230	}
231

    #[test] // [unit->REQ-HAZARD-STALE-SIGNOFF-SENTINEL] a stale signoff sentinel is swept on start.
    fn stale_signoff_swept() {
        let dir = tempfile::tempdir().unwrap();
        let stale = dir.path().join("doyle-signoff.md");
        std::fs::write(&stale, "old signoff").unwrap();

        assert!(sweep_stale_signoff(dir.path(), "doyle").unwrap(), "should sweep one");
        assert!(!stale.exists(), "stale sentinel must be gone");
        // A fresh start with none present sweeps nothing, no error.
        assert!(!sweep_stale_signoff(dir.path(), "doyle").unwrap());
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] boundary writes a provenance-tagged resume commune.
    fn boundary_writes_resume_commune() {
        with_home(|_| {
            let o = write_resume_commune(
                "doyle",
                ParentHint::Infer,
                "where I left off",
                1000,
                DEFAULT_PROTECTION_WINDOW_MS,
            )
            .unwrap();
            assert!(matches!(o, WriteOutcome::Written { .. }));
            let ctx = context_path("doyle", ParentHint::Infer);
            let written = std::fs::read_to_string(&ctx).unwrap();
            assert!(written.contains("Source: self-commune"), "provenance: {written:?}");
            assert!(written.contains("where I left off"));
        });
    }
}


Edit

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] boundary writes a provenance-tagged resume commune.
    fn boundary_writes_resume_commune() {
        with_home(|_| {
            let o = write_resume_commune(
                "doyle",
                ParentHint::Infer,
                "where I left off",
                1000,
                DEFAULT_PROTECTION_WINDOW_MS,
            )
            .unwrap();
            assert!(matches!(o, WriteOutcome::Written { .. }));
            let ctx = context_path("doyle", ParentHint::Infer);
            let written = std::fs::read_to_string(&ctx).unwrap();
            assert!(written.contains("Source: self-commune"), "provenance: {written:?}");
            assert!(written.contains("where I left off"));
        });
    }
}

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] boundary writes a provenance-tagged resume commune.
    fn boundary_writes_resume_commune() {
        with_home(|_| {
            // An untagged Self body routes whole to the live tier (parser fallback).
            let writes = write_resume_commune(
                "doyle",
                "proj-x",
                "where I left off",
                1000,
                DEFAULT_PROTECTION_WINDOW_MS,
            )
            .unwrap();
            assert_eq!(writes.len(), 1);
            assert!(matches!(writes[0].outcome, crate::context::WriteOutcome::Written { .. }));
            let live = spt_store::contextstore::live_context_file(
                &spt_store::perch::tracked_dir(),
                "doyle",
            );
            let written = std::fs::read_to_string(&live).unwrap();
            assert!(written.contains("Source: self-commune"), "provenance: {written:?}");
            assert!(written.contains("where I left off"));

            // A two-slice Self body stamps provenance on BOTH tier files.
            let writes = write_resume_commune(
                "doyle",
                "proj-x",
                "<live-context>L</live-context><project-context>P</project-context>",
                100_000,
                DEFAULT_PROTECTION_WINDOW_MS,
            )
            .unwrap();
            assert_eq!(writes.len(), 2);
            let proj = spt_store::contextstore::project_context_file(
                &spt_store::perch::tracked_dir(),
                "proj-x",
                "doyle",
            );
            assert!(std::fs::read_to_string(&proj).unwrap().contains("Source: self-commune"));
        });
    }
}


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

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] boundary writes a provenance-tagged resume commune.
    fn boundary_writes_resume_commune() {
        with_home(|_| {
            // An untagged Self body routes whole to the live tier (parser fallback).
            let writes = write_resume_commune(
                "doyle",
                "proj-x",
                "where I left off",
                1000,
                DEFAULT_PROTECTION_WINDOW_MS,
            )
            .unwrap();
            assert_eq!(writes.len(), 1);
            assert!(matches!(writes[0].outcome, crate::context::WriteOutcome::Written { .. }));
            let live = spt_store::contextstore::live_context_file(
                &spt_store::perch::tracked_dir(),
                "doyle",
            );
            let written = std::fs::read_to_string(&live).unwrap();
            assert!(written.contains("Source: self-commune"), "provenance: {written:?}");
            assert!(written.contains("where I left off"));

            // A two-slice Self body stamps provenance on BOTH tier files.
            let writes = write_resume_commune(
                "doyle",
                "proj-x",
                "<live-context>L</live-context><project-context>P</project-context>",
                100_000,
                DEFAULT_PROTECTION_WINDOW_MS,
            )
            .unwrap();
            assert_eq!(writes.len(), 2);
            let proj = spt_store::contextstore::project_context_file(
                &spt_store::perch::tracked_dir(),
                "proj-x",
                "doyle",
            );
            assert!(std::fs::read_to_string(&proj).unwrap().contains("Source: self-commune"));
        });
    }
}

//! signoff + boundary lifecycle (M2b T6) — the load-bearing ordering invariants.
//!
//! These are the sister project's most expensive bugs; we **copy the ordering**
//! and clean-room the wiring (the concrete echo/compose/deliver effects are the
//! caller's — T8). Three invariants:
//!
//! - **3.3 echo-before-signoff** ([`graceful_signoff`]): synchronously run the
//!   echo-commune (final context delta) *before* composing INIT_SIGNOFF; skip
//!   the echo only when `session_id` is missing.
//! - **1.1 grace-before-signoff** ([`graceful_signoff`]): the grace wait
//!   completes and the recheck binds `still_gone` *before* any signoff write —
//!   a Self that transiently recovered (binary handoff, brief stale poll) is
//!   **not** torn down.
//! - **3.2 stale-signoff sweep** ([`sweep_stale_signoff`]): on every listener
//!   start, sweep a leftover `<id>-signoff.md` so a prior generation's sentinel
//!   never reads as a live signoff and kills a fresh start.
//!
//! Plus the **boundary Self-resume commune** ([`write_resume_commune`]): M2a's
//! `api boundary` rebinds the rotated session; M2b authors the resume commune
//! the re-entry (T7) preloads.
//
// M2b implements the GRACEFUL teardown paths (spt shutdown / spt suspend /
// boundary). Orphan *crash* detection after an unexpected harness death needs
// the always-on supervisor — that orphan-watch is M3, and it reuses this exact
// ordering. Copy the ordering, not the CC-coupled wiring.
// [impl->REQ-HAZARD-ECHO-BEFORE-SIGNOFF]
// [impl->REQ-HAZARD-GRACE-BEFORE-SIGNOFF]
// [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]

use crate::echo::stamp_provenance;
use crate::ingest::{route_slices, TierWrite};
use spt_proto::envelope::parse_two_slice;
use std::path::Path;
use std::time::Duration;

/// Provenance tag for a Self-authored boundary commune (strong live signal —
/// `docs/CONTEXT-MEMORY.md` commune-source asymmetry).
pub const SOURCE_SELF_COMMUNE: &str = "self-commune";

/// Outcome of a graceful signoff attempt.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SignoffOutcome {
    /// Whether the echo-commune ran (false ⇒ skipped: no session_id).
    pub echo_ran: bool,
    /// Whether the signoff was composed+delivered (false ⇒ the Self recovered
    /// across the grace wait and was spared).
    pub signed_off: bool,
}

/// Run the graceful signoff sequence with the load-bearing ordering baked in.
///
/// The concrete effects are injected so the invariant is testable independent of
/// the echo/compose wiring:
/// - `echo` — run the final-delta echo-commune (3.3);
/// - `recheck_still_gone` — re-evaluate liveness *after* the grace wait (1.1);
/// - `compose_deliver` — compose + deliver INIT_SIGNOFF.
///
/// Order is fixed: `echo` (if `session_id`) → grace wait → `recheck_still_gone`
/// → `compose_deliver` (only if still gone). The recheck binds `still_gone`
/// before any signoff write; the echo precedes the signoff compose.
pub fn graceful_signoff<E>(
    session_id: Option<&str>,
    grace: Duration,
    echo: impl FnOnce() -> Result<(), E>,
    recheck_still_gone: impl FnOnce() -> bool,
    compose_deliver: impl FnOnce() -> Result<(), E>,
) -> Result<SignoffOutcome, E> {
    // 3.3 — echo (final context delta) BEFORE any signoff compose; skip only on
    // a missing session_id.
    let echo_ran = if session_id.is_some() {
        echo()?;
        true
    } else {
        false
    };

    // 1.1 — grace wait, THEN bind still_gone, BEFORE any signoff write.
    if !grace.is_zero() {
        std::thread::sleep(grace);
    }
    let still_gone = recheck_still_gone();

    let signed_off = if still_gone {
        compose_deliver()?;
        true
    } else {
        // The Self recovered across the grace window — never sign off a live Self.
        false
    };

    Ok(SignoffOutcome {
        echo_ran,
        signed_off,
    })
}

/// Sweep a stale `<id>-signoff.md` from `signoff_dir` on listener start (3.2).
/// Returns whether one was removed. Signoff files are write-once per
/// generation; a leftover from a prior session must never read as a live signoff.
pub fn sweep_stale_signoff(signoff_dir: &Path, id: &str) -> std::io::Result<bool> {
    let stale = signoff_dir.join(format!("{id}-signoff.md"));
    match std::fs::remove_file(&stale) {
        Ok(()) => {
            eprintln!("SIGNOFF_SWEEP:{id} (removed stale sentinel)");
            Ok(true)
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
        Err(e) => Err(e),
    }
}

/// Author the **Self-resume commune** at a boundary: stamp `Source: self-commune`
/// provenance on each slice and route it (precedence-guarded, two-tier) into
/// the tracked context store for the re-entry (T7) to preload. The Self
/// authored it, so each write is an llm write. Self communes are the strong
/// **live** signal (commune-source asymmetry), but the routing still honors the
/// tags — a Self that splits its slices gets the split.
// [impl->REQ-INST-2]
pub fn write_resume_commune(
    id: &str,
    project_id: &str,
    body: &str,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<TierWrite>> {
    let slices = parse_two_slice(body);
    let live = slices.live.map(|s| stamp_provenance(SOURCE_SELF_COMMUNE, &s));
    let project = slices.project.map(|s| stamp_provenance(SOURCE_SELF_COMMUNE, &s));
    route_slices(id, project_id, live.as_deref(), project.as_deref(), now_ms, window_ms)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::context::DEFAULT_PROTECTION_WINDOW_MS;
    use crate::test_support::with_home;
    use std::cell::RefCell;

    #[test] // [unit->REQ-HAZARD-ECHO-BEFORE-SIGNOFF] echo runs BEFORE the signoff compose.
    fn echo_precedes_signoff_compose() {
        let log: RefCell<Vec<&str>> = RefCell::new(Vec::new());
        let out = graceful_signoff::<()>(
            Some("sid-1"),
            Duration::ZERO,
            || {
                log.borrow_mut().push("echo");
                Ok(())
            },
            || {
                log.borrow_mut().push("recheck");
                true
            },
            || {
                log.borrow_mut().push("compose");
                Ok(())
            },
        )
        .unwrap();
        assert_eq!(out, SignoffOutcome { echo_ran: true, signed_off: true });
        // Ordering: echo and recheck both strictly precede compose.
        assert_eq!(*log.borrow(), ["echo", "recheck", "compose"]);
    }

    #[test] // [unit->REQ-HAZARD-GRACE-BEFORE-SIGNOFF] a recovered Self is NOT signed off.
    fn recovered_self_is_not_signed_off() {
        let composed = RefCell::new(false);
        let out = graceful_signoff::<()>(
            Some("sid-1"),
            Duration::ZERO,
            || Ok(()),
            || false, // recheck: the Self came back alive
            || {
                *composed.borrow_mut() = true;
                Ok(())
            },
        )
        .unwrap();
        assert_eq!(out, SignoffOutcome { echo_ran: true, signed_off: false });
        assert!(!*composed.borrow(), "must not compose signoff for a live Self");
    }

    #[test] // [unit->REQ-HAZARD-ECHO-BEFORE-SIGNOFF] a missing session_id skips the echo.
    fn missing_session_skips_echo() {
        let echoed = RefCell::new(false);
        let out = graceful_signoff::<()>(
            None,
            Duration::ZERO,
            || {
                *echoed.borrow_mut() = true;
                Ok(())
            },
            || true,
            || Ok(()),
        )
        .unwrap();
        assert!(!out.echo_ran && !*echoed.borrow(), "no echo without a session_id");
        assert!(out.signed_off, "signoff still proceeds when still gone");
    }

    #[test] // [unit->REQ-HAZARD-STALE-SIGNOFF-SENTINEL] a stale signoff sentinel is swept on start.
    fn stale_signoff_swept() {
        let dir = tempfile::tempdir().unwrap();
        let stale = dir.path().join("doyle-signoff.md");
        std::fs::write(&stale, "old signoff").unwrap();

        assert!(sweep_stale_signoff(dir.path(), "doyle").unwrap(), "should sweep one");
        assert!(!stale.exists(), "stale sentinel must be gone");
        // A fresh start with none present sweeps nothing, no error.
        assert!(!sweep_stale_signoff(dir.path(), "doyle").unwrap());
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] boundary writes a provenance-tagged resume commune.
    fn boundary_writes_resume_commune() {
        with_home(|_| {
            let o = write_resume_commune(
                "doyle",
                ParentHint::Infer,
                "where I left off",
                1000,
                DEFAULT_PROTECTION_WINDOW_MS,
            )
            .unwrap();
            assert!(matches!(o, WriteOutcome::Written { .. }));
            let ctx = context_path("doyle", ParentHint::Infer);
            let written = std::fs::read_to_string(&ctx).unwrap();
            assert!(written.contains("Source: self-commune"), "provenance: {written:?}");
            assert!(written.contains("where I left off"));
        });
    }
}

-            let o = write_resume_commune(

+            // An untagged Self body routes whole to the live tier (parser fallback).

+            let writes = write_resume_commune(

-            assert!(matches!(o, WriteOutcome::Written { .. }));

-            let ctx = context_path("doyle", ParentHint::Infer);

-            let written = std::fs::read_to_string(&ctx).unwrap();

+            assert_eq!(writes.len(), 1);

+            assert!(matches!(writes[0].outcome, crate::context::WriteOutcome::Written { .. }));

+            let live = spt_store::contextstore::live_context_file(

+                &spt_store::perch::tracked_dir(),

+            let written = std::fs::read_to_string(&live).unwrap();

+            // A two-slice Self body stamps provenance on BOTH tier files.

+                "<live-context>L</live-context><project-context>P</project-context>",

+                DEFAULT_PROTECTION_WINDOW_MS,

+            assert_eq!(writes.len(), 2);

+            let proj = spt_store::contextstore::project_context_file(

+            assert!(std::fs::read_to_string(&proj).unwrap().contains("Source: self-commune"));


Now resume.rs — compose two-tier psyche-download:


use crate::ingest::context_path;
use crate::psyche::{spawn_and_bind, PsycheError, PsycheHandle};
use spt_store::perch::ParentHint;
use std::collections::BTreeMap;

use crate::psyche::{spawn_and_bind, PsycheError, PsycheHandle};
use spt_store::contextstore::{live_context_file, project_context_file};
use spt_store::perch::tracked_dir;
use std::collections::BTreeMap;


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

use crate::psyche::{spawn_and_bind, PsycheError, PsycheHandle};
use spt_store::contextstore::{live_context_file, project_context_file};
use spt_store::perch::tracked_dir;
use std::collections::BTreeMap;

//! resume-session seam (M2b T7) — re-entry into a Psyche's mind.
//!
//! Two distinct forms (CONTEXT §resume-session seam), both spawned from
//! `[session.psyche_resume]`:
//! - **continue-existing** — resume an existing harness session under the
//!   adapter (its native `--resume {session_id}`). Requires `session_id`.
//! - **fresh-with-preload** — resume with *cleared* context (a fresh session)
//!   plus the **psyche-download**: the prior context, preloaded via the
//!   `{psyche_context}` key (or the adapter pulls it in its SessionStart hook).
//!   Requires `psyche_context`.
//!
//! The required key for the chosen mode is validated **before** spawn — a
//! wrong/missing key errors cleanly, never a half-launched session
//! (REQ-SEAM-RESUME). Both wire into the spawn path and the boundary re-entry
//! (T6 authors the resume commune; this preloads it).
//
// Clean-room over the M2a spawn seam; the native-resume mechanics live in the
// adapter's opaque psyche_resume template (model + --resume flag), never here.
// [impl->REQ-SEAM-RESUME]

use crate::ingest::context_path;
use crate::psyche::{spawn_and_bind, PsycheError, PsycheHandle};
use spt_store::perch::ParentHint;
use std::collections::BTreeMap;

/// The role spawned for a Psyche re-entry.
pub const PSYCHE_RESUME_ROLE: &str = "psyche_resume";

/// Which resume form to perform.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResumeMode {
    /// Native resume of an existing harness session (`{session_id}`).
    ContinueExisting,
    /// Fresh cleared session preloaded with the psyche-download (`{psyche_context}`).
    FreshWithPreload,
}

impl ResumeMode {
    /// The substitution key this mode *requires* the caller to have supplied.
    fn required_key(self) -> &'static str {
        match self {
            ResumeMode::ContinueExisting => "session_id",
            ResumeMode::FreshWithPreload => "psyche_context",
        }
    }
}

/// A resume failure.
#[derive(Debug)]
pub enum ResumeError {
    /// The required key for the chosen mode is missing/empty — caught *before*
    /// any spawn.
    MissingResumeKey {
        mode: ResumeMode,
        key: &'static str,
    },
    /// Spawn / bind failure from the Psyche core.
    Psyche(PsycheError),
}

impl std::fmt::Display for ResumeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ResumeError::MissingResumeKey { mode, key } => {
                write!(f, "resume mode {mode:?} requires key `{key}` (missing before spawn)")
            }
            ResumeError::Psyche(e) => write!(f, "resume spawn failed: {e}"),
        }
    }
}

impl std::error::Error for ResumeError {}

/// Read the perch's stored context — the **psyche-download** a fresh-with-preload
/// resume feeds back in via `{psyche_context}`. `None` if nothing is stored yet.
pub fn download_psyche_context(id: &str, hint: ParentHint<'_>) -> Option<String> {
    std::fs::read_to_string(context_path(id, hint)).ok()
}

/// Resume a Psyche for `parent_id` in the chosen `mode`. The mode's required key
/// is validated against `keys` **before** spawn; on success the `psyche_resume`
/// role is spawned and the nested perch rebound.
pub fn resume_psyche(
    runtime: &dyn spt_runtime::AgentRuntime,
    parent_id: &str,
    mode: ResumeMode,
    keys: &BTreeMap<String, String>,
    event: Option<&str>,
    now: &str,
) -> Result<PsycheHandle, ResumeError> {
    // Validate the mode's required key BEFORE any spawn (REQ-SEAM-RESUME).
    let required = mode.required_key();
    if keys.get(required).map(String::as_str).unwrap_or("").is_empty() {
        return Err(ResumeError::MissingResumeKey {
            mode,
            key: required,
        });
    }
    spawn_and_bind(runtime, PSYCHE_RESUME_ROLE, parent_id, keys, event, now)
        .map_err(ResumeError::Psyche)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::with_home;
    use spt_runtime::{Manifest, ManifestRuntime};

    // psyche_resume templates that record their resume input into a marker file
    // in the cwd (the nested perch dir).
    #[cfg(windows)]
    const RESUME_SID: &str = "cmd /C echo resumed={session_id} > resumed.txt";
    #[cfg(unix)]
    const RESUME_SID: &str = "sh -c \"echo resumed={session_id} > resumed.txt\"";
    #[cfg(windows)]
    const PRELOAD_CTX: &str = "cmd /C echo preload={psyche_context} > preload.txt";
    #[cfg(unix)]
    const PRELOAD_CTX: &str = "sh -c \"echo preload={psyche_context} > preload.txt\"";

    fn resume_manifest(cmd: &str) -> Manifest {
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session.psyche_resume]\ncommand='{cmd}'\ncwd=\"{{psyche_dir}}\"\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    fn keys(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
        pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
    }

    fn wait_marker(path: &std::path::Path) -> String {
        let start = std::time::Instant::now();
        loop {
            if let Ok(s) = std::fs::read_to_string(path) {
                if !s.trim().is_empty() {
                    return s;
                }
            }
            assert!(start.elapsed() < std::time::Duration::from_secs(5), "marker never written");
            std::thread::sleep(std::time::Duration::from_millis(20));
        }
    }

    #[test] // [unit->REQ-SEAM-RESUME] continue-existing re-enters an existing session.
    fn continue_existing_resumes_session() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(RESUME_SID));
            let h = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::ContinueExisting,
                &keys(&[("session_id", "sid-42")]),
                None,
                "t0",
            )
            .expect("resume");
            assert_eq!(h.id, "doyle-psyche");
            let marker = wait_marker(&h.perch_path.join("resumed.txt"));
            assert!(marker.contains("sid-42"), "session id not resumed: {marker:?}");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] fresh-with-preload launches cleared + preloaded.
    fn fresh_with_preload_launches_preloaded() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(PRELOAD_CTX));
            let h = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::FreshWithPreload,
                &keys(&[("psyche_context", "PRIORCTX")]),
                None,
                "t0",
            )
            .expect("resume");
            let marker = wait_marker(&h.perch_path.join("preload.txt"));
            assert!(marker.contains("PRIORCTX"), "context not preloaded: {marker:?}");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] a missing required key errors BEFORE spawn.
    fn missing_key_errors_before_spawn() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(RESUME_SID));
            // ContinueExisting with no session_id.
            let e = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::ContinueExisting,
                &keys(&[]),
                None,
                "t0",
            )
            .unwrap_err();
            assert!(
                matches!(e, ResumeError::MissingResumeKey { key: "session_id", .. }),
                "got {e}"
            );
            // No perch was bound — the error preceded any spawn.
            let perch = spt_store::perch::resolve_perch_path("doyle-psyche", ParentHint::Explicit("doyle"));
            assert!(!perch.join("info.json").exists(), "must not spawn/bind on missing key");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] the psyche-download reads the stored context.
    fn download_reads_stored_context() {
        with_home(|_| {
            assert!(download_psyche_context("doyle", ParentHint::Infer).is_none());
            let ctx = context_path("doyle", ParentHint::Infer);
            std::fs::create_dir_all(ctx.parent().unwrap()).unwrap();
            std::fs::write(&ctx, "stored mind").unwrap();
            assert_eq!(
                download_psyche_context("doyle", ParentHint::Infer).as_deref(),
                Some("stored mind")
            );
        });
    }
}


/// Read the perch's stored context — the **psyche-download** a fresh-with-preload
/// resume feeds back in via `{psyche_context}`. `None` if nothing is stored yet.
pub fn download_psyche_context(id: &str, hint: ParentHint<'_>) -> Option<String> {
    std::fs::read_to_string(context_path(id, hint)).ok()
}

/// Read the stored context — the **psyche-download** a fresh-with-preload
/// resume feeds back in via `{psyche_context}`. Composes the two tracked tiers
/// into the two-slice envelope the Psyche knows how to read (live first, then
/// this project's context). `None` if neither tier holds anything yet.
///
/// A pure read: never creates branches/worktrees (readers must not mint store
/// state) — absent tier files simply contribute nothing.
// [impl->REQ-INST-2]
pub fn download_psyche_context(id: &str, project_id: &str) -> Option<String> {
    let root = tracked_dir();
    let live = std::fs::read_to_string(live_context_file(&root, id)).ok();
    let project = std::fs::read_to_string(project_context_file(&root, project_id, id)).ok();
    if live.is_none() && project.is_none() {
        return None;
    }
    let mut out = String::new();
    if let Some(l) = live {
        out.push_str("<live-context>\n");
        out.push_str(l.trim_end());
        out.push_str("\n</live-context>\n");
    }
    if let Some(p) = project {
        out.push_str("<project-context>\n");
        out.push_str(p.trim_end());
        out.push_str("\n</project-context>\n");
    }
    Some(out)
}


/// Read the stored context — the **psyche-download** a fresh-with-preload
/// resume feeds back in via `{psyche_context}`. Composes the two tracked tiers
/// into the two-slice envelope the Psyche knows how to read (live first, then
/// this project's context). `None` if neither tier holds anything yet.
///
/// A pure read: never creates branches/worktrees (readers must not mint store
/// state) — absent tier files simply contribute nothing.
// [impl->REQ-INST-2]
pub fn download_psyche_context(id: &str, project_id: &str) -> Option<String> {
    let root = tracked_dir();
    let live = std::fs::read_to_string(live_context_file(&root, id)).ok();
    let project = std::fs::read_to_string(project_context_file(&root, project_id, id)).ok();
    if live.is_none() && project.is_none() {
        return None;
    }
    let mut out = String::new();
    if let Some(l) = live {
        out.push_str("<live-context>\n");
        out.push_str(l.trim_end());
        out.push_str("\n</live-context>\n");
    }
    if let Some(p) = project {
        out.push_str("<project-context>\n");
        out.push_str(p.trim_end());
        out.push_str("\n</project-context>\n");
    }
    Some(out)
}

//! resume-session seam (M2b T7) — re-entry into a Psyche's mind.
//!
//! Two distinct forms (CONTEXT §resume-session seam), both spawned from
//! `[session.psyche_resume]`:
//! - **continue-existing** — resume an existing harness session under the
//!   adapter (its native `--resume {session_id}`). Requires `session_id`.
//! - **fresh-with-preload** — resume with *cleared* context (a fresh session)
//!   plus the **psyche-download**: the prior context, preloaded via the
//!   `{psyche_context}` key (or the adapter pulls it in its SessionStart hook).
//!   Requires `psyche_context`.
//!
//! The required key for the chosen mode is validated **before** spawn — a
//! wrong/missing key errors cleanly, never a half-launched session
//! (REQ-SEAM-RESUME). Both wire into the spawn path and the boundary re-entry
//! (T6 authors the resume commune; this preloads it).
//
// Clean-room over the M2a spawn seam; the native-resume mechanics live in the
// adapter's opaque psyche_resume template (model + --resume flag), never here.
// [impl->REQ-SEAM-RESUME]

use crate::psyche::{spawn_and_bind, PsycheError, PsycheHandle};
use spt_store::contextstore::{live_context_file, project_context_file};
use spt_store::perch::tracked_dir;
use std::collections::BTreeMap;

/// The role spawned for a Psyche re-entry.
pub const PSYCHE_RESUME_ROLE: &str = "psyche_resume";

/// Which resume form to perform.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResumeMode {
    /// Native resume of an existing harness session (`{session_id}`).
    ContinueExisting,
    /// Fresh cleared session preloaded with the psyche-download (`{psyche_context}`).
    FreshWithPreload,
}

impl ResumeMode {
    /// The substitution key this mode *requires* the caller to have supplied.
    fn required_key(self) -> &'static str {
        match self {
            ResumeMode::ContinueExisting => "session_id",
            ResumeMode::FreshWithPreload => "psyche_context",
        }
    }
}

/// A resume failure.
#[derive(Debug)]
pub enum ResumeError {
    /// The required key for the chosen mode is missing/empty — caught *before*
    /// any spawn.
    MissingResumeKey {
        mode: ResumeMode,
        key: &'static str,
    },
    /// Spawn / bind failure from the Psyche core.
    Psyche(PsycheError),
}

impl std::fmt::Display for ResumeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ResumeError::MissingResumeKey { mode, key } => {
                write!(f, "resume mode {mode:?} requires key `{key}` (missing before spawn)")
            }
            ResumeError::Psyche(e) => write!(f, "resume spawn failed: {e}"),
        }
    }
}

impl std::error::Error for ResumeError {}

/// Read the perch's stored context — the **psyche-download** a fresh-with-preload
/// resume feeds back in via `{psyche_context}`. `None` if nothing is stored yet.
pub fn download_psyche_context(id: &str, hint: ParentHint<'_>) -> Option<String> {
    std::fs::read_to_string(context_path(id, hint)).ok()
}

/// Resume a Psyche for `parent_id` in the chosen `mode`. The mode's required key
/// is validated against `keys` **before** spawn; on success the `psyche_resume`
/// role is spawned and the nested perch rebound.
pub fn resume_psyche(
    runtime: &dyn spt_runtime::AgentRuntime,
    parent_id: &str,
    mode: ResumeMode,
    keys: &BTreeMap<String, String>,
    event: Option<&str>,
    now: &str,
) -> Result<PsycheHandle, ResumeError> {
    // Validate the mode's required key BEFORE any spawn (REQ-SEAM-RESUME).
    let required = mode.required_key();
    if keys.get(required).map(String::as_str).unwrap_or("").is_empty() {
        return Err(ResumeError::MissingResumeKey {
            mode,
            key: required,
        });
    }
    spawn_and_bind(runtime, PSYCHE_RESUME_ROLE, parent_id, keys, event, now)
        .map_err(ResumeError::Psyche)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::with_home;
    use spt_runtime::{Manifest, ManifestRuntime};

    // psyche_resume templates that record their resume input into a marker file
    // in the cwd (the nested perch dir).
    #[cfg(windows)]
    const RESUME_SID: &str = "cmd /C echo resumed={session_id} > resumed.txt";
    #[cfg(unix)]
    const RESUME_SID: &str = "sh -c \"echo resumed={session_id} > resumed.txt\"";
    #[cfg(windows)]
    const PRELOAD_CTX: &str = "cmd /C echo preload={psyche_context} > preload.txt";
    #[cfg(unix)]
    const PRELOAD_CTX: &str = "sh -c \"echo preload={psyche_context} > preload.txt\"";

    fn resume_manifest(cmd: &str) -> Manifest {
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session.psyche_resume]\ncommand='{cmd}'\ncwd=\"{{psyche_dir}}\"\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    fn keys(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
        pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
    }

    fn wait_marker(path: &std::path::Path) -> String {
        let start = std::time::Instant::now();
        loop {
            if let Ok(s) = std::fs::read_to_string(path) {
                if !s.trim().is_empty() {
                    return s;
                }
            }
            assert!(start.elapsed() < std::time::Duration::from_secs(5), "marker never written");
            std::thread::sleep(std::time::Duration::from_millis(20));
        }
    }

    #[test] // [unit->REQ-SEAM-RESUME] continue-existing re-enters an existing session.
    fn continue_existing_resumes_session() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(RESUME_SID));
            let h = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::ContinueExisting,
                &keys(&[("session_id", "sid-42")]),
                None,
                "t0",
            )
            .expect("resume");
            assert_eq!(h.id, "doyle-psyche");
            let marker = wait_marker(&h.perch_path.join("resumed.txt"));
            assert!(marker.contains("sid-42"), "session id not resumed: {marker:?}");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] fresh-with-preload launches cleared + preloaded.
    fn fresh_with_preload_launches_preloaded() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(PRELOAD_CTX));
            let h = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::FreshWithPreload,
                &keys(&[("psyche_context", "PRIORCTX")]),
                None,
                "t0",
            )
            .expect("resume");
            let marker = wait_marker(&h.perch_path.join("preload.txt"));
            assert!(marker.contains("PRIORCTX"), "context not preloaded: {marker:?}");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] a missing required key errors BEFORE spawn.
    fn missing_key_errors_before_spawn() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(RESUME_SID));
            // ContinueExisting with no session_id.
            let e = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::ContinueExisting,
                &keys(&[]),
                None,
                "t0",
            )
            .unwrap_err();
            assert!(
                matches!(e, ResumeError::MissingResumeKey { key: "session_id", .. }),
                "got {e}"
            );
            // No perch was bound — the error preceded any spawn.
            let perch = spt_store::perch::resolve_perch_path("doyle-psyche", ParentHint::Explicit("doyle"));
            assert!(!perch.join("info.json").exists(), "must not spawn/bind on missing key");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] the psyche-download reads the stored context.
    fn download_reads_stored_context() {
        with_home(|_| {
            assert!(download_psyche_context("doyle", ParentHint::Infer).is_none());
            let ctx = context_path("doyle", ParentHint::Infer);
            std::fs::create_dir_all(ctx.parent().unwrap()).unwrap();
            std::fs::write(&ctx, "stored mind").unwrap();
            assert_eq!(
                download_psyche_context("doyle", ParentHint::Infer).as_deref(),
                Some("stored mind")
            );
        });
    }
}

-/// Read the perch's stored context — the **psyche-download** a fresh-with-preload

-/// resume feeds back in via `{psyche_context}`. `None` if nothing is stored yet.

-pub fn download_psyche_context(id: &str, hint: ParentHint<'_>) -> Option<String> {

-    std::fs::read_to_string(context_path(id, hint)).ok()

+/// Read the stored context — the **psyche-download** a fresh-with-preload

+/// resume feeds back in via `{psyche_context}`. Composes the two tracked tiers

+/// into the two-slice envelope the Psyche knows how to read (live first, then

+/// this project's context). `None` if neither tier holds anything yet.

+/// A pure read: never creates branches/worktrees (readers must not mint store

+/// state) — absent tier files simply contribute nothing.

+pub fn download_psyche_context(id: &str, project_id: &str) -> Option<String> {

+    let live = std::fs::read_to_string(live_context_file(&root, id)).ok();

+    let project = std::fs::read_to_string(project_context_file(&root, project_id, id)).ok();

+    if live.is_none() && project.is_none() {

+        out.push_str("<live-context>\n");

+        out.push_str("\n</live-context>\n");

+        out.push_str("<project-context>\n");

+        out.push_str("\n</project-context>\n");


Update resume tests (download test + ParentHint use):


    #[test] // [unit->REQ-SEAM-RESUME] the psyche-download reads the stored context.
    fn download_reads_stored_context() {
        with_home(|_| {
            assert!(download_psyche_context("doyle", ParentHint::Infer).is_none());
            let ctx = context_path("doyle", ParentHint::Infer);
            std::fs::create_dir_all(ctx.parent().unwrap()).unwrap();
            std::fs::write(&ctx, "stored mind").unwrap();
            assert_eq!(
                download_psyche_context("doyle", ParentHint::Infer).as_deref(),
                Some("stored mind")
            );
        });
    }

    #[test] // [unit->REQ-INST-2] the psyche-download composes the two tracked tiers.
    fn download_composes_two_tiers() {
        with_home(|_| {
            assert!(download_psyche_context("doyle", "proj-x").is_none());

            // Seed both tiers through the store (the writer path).
            let cs = spt_store::contextstore::ContextStore::open_or_init().unwrap();
            std::fs::write(cs.live_context_path("doyle").unwrap(), "live mind").unwrap();
            std::fs::write(
                cs.project_context_path("proj-x", "doyle").unwrap(),
                "project mind",
            )
            .unwrap();

            let got = download_psyche_context("doyle", "proj-x").expect("composed");
            let slices = spt_proto::envelope::parse_two_slice(&got);
            assert_eq!(slices.live.as_deref(), Some("live mind"));
            assert_eq!(slices.project.as_deref(), Some("project mind"));

            // A different project sees the live tier only (no cross-project leak).
            let other = download_psyche_context("doyle", "other-proj").expect("live only");
            let slices = spt_proto::envelope::parse_two_slice(&other);
            assert_eq!(slices.live.as_deref(), Some("live mind"));
            assert_eq!(slices.project, None);
        });
    }


    #[test] // [unit->REQ-INST-2] the psyche-download composes the two tracked tiers.
    fn download_composes_two_tiers() {
        with_home(|_| {
            assert!(download_psyche_context("doyle", "proj-x").is_none());

            // Seed both tiers through the store (the writer path).
            let cs = spt_store::contextstore::ContextStore::open_or_init().unwrap();
            std::fs::write(cs.live_context_path("doyle").unwrap(), "live mind").unwrap();
            std::fs::write(
                cs.project_context_path("proj-x", "doyle").unwrap(),
                "project mind",
            )
            .unwrap();

            let got = download_psyche_context("doyle", "proj-x").expect("composed");
            let slices = spt_proto::envelope::parse_two_slice(&got);
            assert_eq!(slices.live.as_deref(), Some("live mind"));
            assert_eq!(slices.project.as_deref(), Some("project mind"));

            // A different project sees the live tier only (no cross-project leak).
            let other = download_psyche_context("doyle", "other-proj").expect("live only");
            let slices = spt_proto::envelope::parse_two_slice(&other);
            assert_eq!(slices.live.as_deref(), Some("live mind"));
            assert_eq!(slices.project, None);
        });
    }

//! resume-session seam (M2b T7) — re-entry into a Psyche's mind.
//!
//! Two distinct forms (CONTEXT §resume-session seam), both spawned from
//! `[session.psyche_resume]`:
//! - **continue-existing** — resume an existing harness session under the
//!   adapter (its native `--resume {session_id}`). Requires `session_id`.
//! - **fresh-with-preload** — resume with *cleared* context (a fresh session)
//!   plus the **psyche-download**: the prior context, preloaded via the
//!   `{psyche_context}` key (or the adapter pulls it in its SessionStart hook).
//!   Requires `psyche_context`.
//!
//! The required key for the chosen mode is validated **before** spawn — a
//! wrong/missing key errors cleanly, never a half-launched session
//! (REQ-SEAM-RESUME). Both wire into the spawn path and the boundary re-entry
//! (T6 authors the resume commune; this preloads it).
//
// Clean-room over the M2a spawn seam; the native-resume mechanics live in the
// adapter's opaque psyche_resume template (model + --resume flag), never here.
// [impl->REQ-SEAM-RESUME]

use crate::psyche::{spawn_and_bind, PsycheError, PsycheHandle};
use spt_store::contextstore::{live_context_file, project_context_file};
use spt_store::perch::tracked_dir;
use std::collections::BTreeMap;

/// The role spawned for a Psyche re-entry.
pub const PSYCHE_RESUME_ROLE: &str = "psyche_resume";

/// Which resume form to perform.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResumeMode {
    /// Native resume of an existing harness session (`{session_id}`).
    ContinueExisting,
    /// Fresh cleared session preloaded with the psyche-download (`{psyche_context}`).
    FreshWithPreload,
}

impl ResumeMode {
    /// The substitution key this mode *requires* the caller to have supplied.
    fn required_key(self) -> &'static str {
        match self {
            ResumeMode::ContinueExisting => "session_id",
            ResumeMode::FreshWithPreload => "psyche_context",
        }
    }
}

/// A resume failure.
#[derive(Debug)]
pub enum ResumeError {
    /// The required key for the chosen mode is missing/empty — caught *before*
    /// any spawn.
    MissingResumeKey {
        mode: ResumeMode,
        key: &'static str,
    },
    /// Spawn / bind failure from the Psyche core.
    Psyche(PsycheError),
}

impl std::fmt::Display for ResumeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ResumeError::MissingResumeKey { mode, key } => {
                write!(f, "resume mode {mode:?} requires key `{key}` (missing before spawn)")
            }
            ResumeError::Psyche(e) => write!(f, "resume spawn failed: {e}"),
        }
    }
}

impl std::error::Error for ResumeError {}

/// Read the stored context — the **psyche-download** a fresh-with-preload
/// resume feeds back in via `{psyche_context}`. Composes the two tracked tiers
/// into the two-slice envelope the Psyche knows how to read (live first, then
/// this project's context). `None` if neither tier holds anything yet.
///
/// A pure read: never creates branches/worktrees (readers must not mint store
/// state) — absent tier files simply contribute nothing.
// [impl->REQ-INST-2]
pub fn download_psyche_context(id: &str, project_id: &str) -> Option<String> {
    let root = tracked_dir();
    let live = std::fs::read_to_string(live_context_file(&root, id)).ok();
    let project = std::fs::read_to_string(project_context_file(&root, project_id, id)).ok();
    if live.is_none() && project.is_none() {
        return None;
    }
    let mut out = String::new();
    if let Some(l) = live {
        out.push_str("<live-context>\n");
        out.push_str(l.trim_end());
        out.push_str("\n</live-context>\n");
    }
    if let Some(p) = project {
        out.push_str("<project-context>\n");
        out.push_str(p.trim_end());
        out.push_str("\n</project-context>\n");
    }
    Some(out)
}

/// Resume a Psyche for `parent_id` in the chosen `mode`. The mode's required key
/// is validated against `keys` **before** spawn; on success the `psyche_resume`
/// role is spawned and the nested perch rebound.
pub fn resume_psyche(
    runtime: &dyn spt_runtime::AgentRuntime,
    parent_id: &str,
    mode: ResumeMode,
    keys: &BTreeMap<String, String>,
    event: Option<&str>,
    now: &str,
) -> Result<PsycheHandle, ResumeError> {
    // Validate the mode's required key BEFORE any spawn (REQ-SEAM-RESUME).
    let required = mode.required_key();
    if keys.get(required).map(String::as_str).unwrap_or("").is_empty() {
        return Err(ResumeError::MissingResumeKey {
            mode,
            key: required,
        });
    }
    spawn_and_bind(runtime, PSYCHE_RESUME_ROLE, parent_id, keys, event, now)
        .map_err(ResumeError::Psyche)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::with_home;
    use spt_runtime::{Manifest, ManifestRuntime};

    // psyche_resume templates that record their resume input into a marker file
    // in the cwd (the nested perch dir).
    #[cfg(windows)]
    const RESUME_SID: &str = "cmd /C echo resumed={session_id} > resumed.txt";
    #[cfg(unix)]
    const RESUME_SID: &str = "sh -c \"echo resumed={session_id} > resumed.txt\"";
    #[cfg(windows)]
    const PRELOAD_CTX: &str = "cmd /C echo preload={psyche_context} > preload.txt";
    #[cfg(unix)]
    const PRELOAD_CTX: &str = "sh -c \"echo preload={psyche_context} > preload.txt\"";

    fn resume_manifest(cmd: &str) -> Manifest {
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session.psyche_resume]\ncommand='{cmd}'\ncwd=\"{{psyche_dir}}\"\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    fn keys(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
        pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
    }

    fn wait_marker(path: &std::path::Path) -> String {
        let start = std::time::Instant::now();
        loop {
            if let Ok(s) = std::fs::read_to_string(path) {
                if !s.trim().is_empty() {
                    return s;
                }
            }
            assert!(start.elapsed() < std::time::Duration::from_secs(5), "marker never written");
            std::thread::sleep(std::time::Duration::from_millis(20));
        }
    }

    #[test] // [unit->REQ-SEAM-RESUME] continue-existing re-enters an existing session.
    fn continue_existing_resumes_session() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(RESUME_SID));
            let h = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::ContinueExisting,
                &keys(&[("session_id", "sid-42")]),
                None,
                "t0",
            )
            .expect("resume");
            assert_eq!(h.id, "doyle-psyche");
            let marker = wait_marker(&h.perch_path.join("resumed.txt"));
            assert!(marker.contains("sid-42"), "session id not resumed: {marker:?}");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] fresh-with-preload launches cleared + preloaded.
    fn fresh_with_preload_launches_preloaded() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(PRELOAD_CTX));
            let h = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::FreshWithPreload,
                &keys(&[("psyche_context", "PRIORCTX")]),
                None,
                "t0",
            )
            .expect("resume");
            let marker = wait_marker(&h.perch_path.join("preload.txt"));
            assert!(marker.contains("PRIORCTX"), "context not preloaded: {marker:?}");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] a missing required key errors BEFORE spawn.
    fn missing_key_errors_before_spawn() {
        with_home(|_| {
            let rt = ManifestRuntime::new(resume_manifest(RESUME_SID));
            // ContinueExisting with no session_id.
            let e = resume_psyche(
                &rt,
                "doyle",
                ResumeMode::ContinueExisting,
                &keys(&[]),
                None,
                "t0",
            )
            .unwrap_err();
            assert!(
                matches!(e, ResumeError::MissingResumeKey { key: "session_id", .. }),
                "got {e}"
            );
            // No perch was bound — the error preceded any spawn.
            let perch = spt_store::perch::resolve_perch_path("doyle-psyche", ParentHint::Explicit("doyle"));
            assert!(!perch.join("info.json").exists(), "must not spawn/bind on missing key");
        });
    }

    #[test] // [unit->REQ-SEAM-RESUME] the psyche-download reads the stored context.
    fn download_reads_stored_context() {
        with_home(|_| {
            assert!(download_psyche_context("doyle", ParentHint::Infer).is_none());
            let ctx = context_path("doyle", ParentHint::Infer);
            std::fs::create_dir_all(ctx.parent().unwrap()).unwrap();
            std::fs::write(&ctx, "stored mind").unwrap();
            assert_eq!(
                download_psyche_context("doyle", ParentHint::Infer).as_deref(),
                Some("stored mind")
            );
        });
    }
}

-    #[test] // [unit->REQ-SEAM-RESUME] the psyche-download reads the stored context.

-    fn download_reads_stored_context() {

+    #[test] // [unit->REQ-INST-2] the psyche-download composes the two tracked tiers.

-            assert!(download_psyche_context("doyle", ParentHint::Infer).is_none());

-            let ctx = context_path("doyle", ParentHint::Infer);

-            std::fs::create_dir_all(ctx.parent().unwrap()).unwrap();

-            std::fs::write(&ctx, "stored mind").unwrap();

-                download_psyche_context("doyle", ParentHint::Infer).as_deref(),

+            assert!(download_psyche_context("doyle", "proj-x").is_none());

+            // Seed both tiers through the store (the writer path).

+            let cs = spt_store::contextstore::ContextStore::open_or_init().unwrap();

+            std::fs::write(cs.live_context_path("doyle").unwrap(), "live mind").unwrap();

+                cs.project_context_path("proj-x", "doyle").unwrap(),

+            let got = download_psyche_context("doyle", "proj-x").expect("composed");

+            let slices = spt_proto::envelope::parse_two_slice(&got);

+            assert_eq!(slices.live.as_deref(), Some("live mind"));

+            assert_eq!(slices.project.as_deref(), Some("project mind"));

+            // A different project sees the live tier only (no cross-project leak).

+            let other = download_psyche_context("doyle", "other-proj").expect("live only");

+            let slices = spt_proto::envelope::parse_two_slice(&other);

+            assert_eq!(slices.project, None);


D6a-1: BranchStore + ContextStore + project_id in spt-store

gitrun.rs (bounded git, 5.3), branchstore.rs (git-CLI BranchStore: bare seed, ensure branch/worktree, commit=checkpoint/tip=resume, REQ-STORE-1 activate), contextstore.rs (tracked/ two-tier layout, a-/p- branches), project.rs (project_id from git remote / folder fallback), perch.rs tracked paths, STORAGE.md seed-repo note. Push CI-green.

D6a-2: two-tier ingest routing + rename arm in spt-live

ingest.rs routes commune slices (parse_two_slice) into ContextStore tier files (precedence-guarded + committed); lifecycle wiring (project dir → project_id); spt rename context-branch arm (a-<old>→a-<new>); REQ-INST-2 activate [impl,unit]. Push CI-green.

D6b: vector merge + Psyche reconcile + D7.5a driver

VersionVector + marker v2 (node+vector), per-file merge fn, conflict artifacts (REQ-HAZARD-CONFLICT-BOTH-PRESERVED register+activate), reconciler election, bounded stdout-captured Psyche turn driver (D7.5a pulled forward), reconcile flow + validation + retry + event seam.

D6c: bundle sync over broker streams + gates

SyncRequest/bundle-chunk records, pull-based ref-scoped (synced(E,S)+visibility server-side fail-closed), fetch+driver merge+two-parent commits, triggers (write-notify, activation freshness feed, anti-entropy), loopback two-daemon tests, FAULT-MATRIX rows. REQ-NET-3 + REQ-INST-5 activate.


Pulse + lib exports:


/// Run one pulse tick for `id`: ingest drops from `drop_dir`, then read+clear
/// the echo-gate (once-only fire) and the idle flag. `now_ms` stamps the
/// precedence markers of any ingested writes.
pub fn tick(
    id: &str,
    hint: ParentHint<'_>,
    drop_dir: &Path,
    now_ms: u64,
    cfg: &PulseConfig,
) -> std::io::Result<TickOutcome> {
    let ingested = ingest_drops(drop_dir, id, hint.clone(), now_ms, cfg.protection_window_ms)?;
    let echo_due = take_echo_gate(id, hint.clone());
    let idle = resolve_idle_file(id, hint).exists();

/// Run one pulse tick for `id`: ingest drops from `drop_dir` (two-tier routed
/// under `project_id`), then read+clear the echo-gate (once-only fire) and the
/// idle flag. `now_ms` stamps the precedence markers of any ingested writes.
pub fn tick(
    id: &str,
    project_id: &str,
    hint: ParentHint<'_>,
    drop_dir: &Path,
    now_ms: u64,
    cfg: &PulseConfig,
) -> std::io::Result<TickOutcome> {
    let ingested = ingest_drops(drop_dir, id, project_id, now_ms, cfg.protection_window_ms)?;
    let echo_due = take_echo_gate(id, hint.clone());
    let idle = resolve_idle_file(id, hint).exists();


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

/// Run one pulse tick for `id`: ingest drops from `drop_dir` (two-tier routed
/// under `project_id`), then read+clear the echo-gate (once-only fire) and the
/// idle flag. `now_ms` stamps the precedence markers of any ingested writes.
pub fn tick(
    id: &str,
    project_id: &str,
    hint: ParentHint<'_>,
    drop_dir: &Path,
    now_ms: u64,
    cfg: &PulseConfig,
) -> std::io::Result<TickOutcome> {
    let ingested = ingest_drops(drop_dir, id, project_id, now_ms, cfg.protection_window_ms)?;
    let echo_due = take_echo_gate(id, hint.clone());
    let idle = resolve_idle_file(id, hint).exists();

//! pulse heartbeat (M2b T5) — the interim per-tick driver.
//!
//! In the no-daemon interim model, the long-running `api listen` process
//! (T8) hosts a pulse loop. Each tick does the lightweight, recurring work:
//! - **poll the drop dirs** → fold any commune/signoff drops into context
//!   (single-writer ingest→delete — T4);
//! - **fire the pending echo** → if the echo-gate (`.more-done`, armed by
//!   `api state idle`) is set, signal that an echo is due and **clear the gate**
//!   so it fires *exactly once*;
//! - surface the **idle** flag for the caller's capsule/idle decisions.
//!
//! The pulse is deliberately thin — it decides *when* an echo is due and
//! guarantees once-only; the heavy echo run ([`crate::echo::run_echo_commune`])
//! is the caller's (T8), which holds the runtime + history + commune dir. Keep
//! all of this behind this one module so M3's daemon re-host (Psyche-as-loop,
//! daemon-owned period) is a localized swap, not a rewrite (mirrors M2a's
//! seed-file discipline).
//
// Interim only: the durable pulse loop + its global/per-endpoint period config
// belong to the M3 daemon (CONTEXT R-DAEMON-1). No dedicated REQ — the pulse is
// throwaway glue under REQ-SEAM-PSYCHE (see M2b-PLAN §New requirements).

use crate::context::DEFAULT_PROTECTION_WINDOW_MS;
use crate::ingest::{ingest_drops, Ingested};
use spt_store::perch::{resolve_echo_gate_file, resolve_idle_file, ParentHint};
use std::path::Path;
use std::time::Duration;

/// Interim default pulse period. The durable, configurable knob (global +
/// per-endpoint override) is the M3 daemon's; this is the interim constant.
pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);

/// Pulse tuning. `period` paces the `api listen` loop; `protection_window_ms`
/// is threaded into the drop-file precedence guard (6.5).
#[derive(Debug, Clone, Copy)]
pub struct PulseConfig {
    pub period: Duration,
    pub protection_window_ms: u64,
}

impl Default for PulseConfig {
    fn default() -> Self {
        PulseConfig {
            period: DEFAULT_PULSE_PERIOD,
            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,
        }
    }
}

/// What one tick observed/did. `echo_due` means the gate was armed and has now
/// been cleared — the caller should fire exactly one echo-commune.
#[derive(Debug, Clone)]
pub struct TickOutcome {
    pub ingested: Vec<Ingested>,
    pub echo_due: bool,
    pub idle: bool,
}

/// Run one pulse tick for `id`: ingest drops from `drop_dir`, then read+clear
/// the echo-gate (once-only fire) and the idle flag. `now_ms` stamps the
/// precedence markers of any ingested writes.
pub fn tick(
    id: &str,
    hint: ParentHint<'_>,
    drop_dir: &Path,
    now_ms: u64,
    cfg: &PulseConfig,
) -> std::io::Result<TickOutcome> {
    let ingested = ingest_drops(drop_dir, id, hint.clone(), now_ms, cfg.protection_window_ms)?;
    let echo_due = take_echo_gate(id, hint.clone());
    let idle = resolve_idle_file(id, hint).exists();
    Ok(TickOutcome {
        ingested,
        echo_due,
        idle,
    })
}

/// Read-and-clear the echo-gate: returns whether it was armed, removing the
/// sentinel so the echo fires exactly once. Clearing a missing gate is a no-op.
pub fn take_echo_gate(id: &str, hint: ParentHint<'_>) -> bool {
    let gate = resolve_echo_gate_file(id, hint);
    if gate.exists() {
        // Best-effort clear; if removal fails the next tick re-fires (at-least-
        // once degrades safely — a duplicate echo is harmless, a lost one less so).
        let _ = std::fs::remove_file(&gate);
        true
    } else {
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::with_home;
    use spt_store::perch::resolve_perch_path;

    fn arm_gate(id: &str) {
        let perch = resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&perch).unwrap();
        std::fs::write(resolve_echo_gate_file(id, ParentHint::Infer), "").unwrap();
    }

    #[test] // [unit->REQ-SEAM-PSYCHE] an armed echo-gate fires exactly once, then clears.
    fn echo_gate_fires_exactly_once() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            arm_gate("doyle");

            let t1 = tick("doyle", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())
                .expect("tick");
            assert!(t1.echo_due, "armed gate should fire");

            // Second tick: gate cleared, no re-fire.
            let t2 = tick("doyle", ParentHint::Infer, drops.path(), 2000, &PulseConfig::default())
                .expect("tick");
            assert!(!t2.echo_due, "gate must clear after one fire");
        });
    }

    #[test] // [unit->REQ-SEAM-PSYCHE] a tick polls + ingests the drop dir.
    fn tick_ingests_drops() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            std::fs::write(drops.path().join("doyle-commune.md"), "brief").unwrap();

            let t = tick("doyle", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())
                .expect("tick");
            assert_eq!(t.ingested.len(), 1);
            assert!(!t.echo_due, "no gate armed");
            assert!(!drops.path().join("doyle-commune.md").exists(), "drop consumed");
        });
    }

    #[test] // [unit->REQ-SEAM-PSYCHE] idle sentinel surfaces in the tick outcome.
    fn tick_reports_idle() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let perch = resolve_perch_path("doyle", ParentHint::Infer);
            std::fs::create_dir_all(&perch).unwrap();
            std::fs::write(resolve_idle_file("doyle", ParentHint::Infer), "").unwrap();

            let t = tick("doyle", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())
                .expect("tick");
            assert!(t.idle);
        });
    }
}

-/// Run one pulse tick for `id`: ingest drops from `drop_dir`, then read+clear

-/// the echo-gate (once-only fire) and the idle flag. `now_ms` stamps the

-/// precedence markers of any ingested writes.

+/// Run one pulse tick for `id`: ingest drops from `drop_dir` (two-tier routed

+/// under `project_id`), then read+clear the echo-gate (once-only fire) and the

+/// idle flag. `now_ms` stamps the precedence markers of any ingested writes.

-    let ingested = ingest_drops(drop_dir, id, hint.clone(), now_ms, cfg.protection_window_ms)?;

+    let ingested = ingest_drops(drop_dir, id, project_id, now_ms, cfg.protection_window_ms)?;


Bash

cd C:/Users/decid/Documents/projects/spt-core && sed -i 's|let t1 = tick("doyle", ParentHint::Infer, drops.path(), 1000, \&PulseConfig::default())|let t1 = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 1000, \&PulseConfig::default())|; s|let t2 = tick("doyle", ParentHint::Infer, drops.path(), 2000, \&PulseConfig::default())|let t2 = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 2000, \&PulseConfig::default())|; s|let t = tick("doyle", ParentHint::Infer, drops.path(), 1000, \&PulseConfig::default())|let t = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 1000, \&PulseConfig::default())|g' crates/spt-live/src/pulse.rs && grep -n 'tick("doyle"' crates/spt-live/src/pulse.rs

Update pulse test call sites


112:            let t1 = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())
117:            let t2 = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 2000, &PulseConfig::default())
129:            let t = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())
145:            let t = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())


1	//! pulse heartbeat (M2b T5) — the interim per-tick driver.
2	//!
3	//! In the no-daemon interim model, the long-running `api listen` process
4	//! (T8) hosts a pulse loop. Each tick does the lightweight, recurring work:
5	//! - **poll the drop dirs** → fold any commune/signoff drops into context
6	//!   (single-writer ingest→delete — T4);
7	//! - **fire the pending echo** → if the echo-gate (`.more-done`, armed by
8	//!   `api state idle`) is set, signal that an echo is due and **clear the gate**
9	//!   so it fires *exactly once*;
10	//! - surface the **idle** flag for the caller's capsule/idle decisions.
11	//!
12	//! The pulse is deliberately thin — it decides *when* an echo is due and
13	//! guarantees once-only; the heavy echo run ([`crate::echo::run_echo_commune`])
14	//! is the caller's (T8), which holds the runtime + history + commune dir. Keep
15	//! all of this behind this one module so M3's daemon re-host (Psyche-as-loop,
16	//! daemon-owned period) is a localized swap, not a rewrite (mirrors M2a's
17	//! seed-file discipline).
18	//
19	// Interim only: the durable pulse loop + its global/per-endpoint period config
20	// belong to the M3 daemon (CONTEXT R-DAEMON-1). No dedicated REQ — the pulse is
21	// throwaway glue under REQ-SEAM-PSYCHE (see M2b-PLAN §New requirements).
22
23	use crate::context::DEFAULT_PROTECTION_WINDOW_MS;
24	use crate::ingest::{ingest_drops, Ingested};
25	use spt_store::perch::{resolve_echo_gate_file, resolve_idle_file, ParentHint};
26	use std::path::Path;
27	use std::time::Duration;
28
29	/// Interim default pulse period. The durable, configurable knob (global +
30	/// per-endpoint override) is the M3 daemon's; this is the interim constant.
31	pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);
32
33	/// Pulse tuning. `period` paces the `api listen` loop; `protection_window_ms`
34	/// is threaded into the drop-file precedence guard (6.5).
35	#[derive(Debug, Clone, Copy)]
36	pub struct PulseConfig {
37	    pub period: Duration,
38	    pub protection_window_ms: u64,
39	}
40
41	impl Default for PulseConfig {
42	    fn default() -> Self {
43	        PulseConfig {
44	            period: DEFAULT_PULSE_PERIOD,
45	            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,
46	        }
47	    }
48	}
49
50	/// What one tick observed/did. `echo_due` means the gate was armed and has now
51	/// been cleared — the caller should fire exactly one echo-commune.
52	#[derive(Debug, Clone)]
53	pub struct TickOutcome {
54	    pub ingested: Vec<Ingested>,
55	    pub echo_due: bool,
56	    pub idle: bool,
57	}
58
59	/// Run one pulse tick for `id`: ingest drops from `drop_dir` (two-tier routed
60	/// under `project_id`), then read+clear the echo-gate (once-only fire) and the
61	/// idle flag. `now_ms` stamps the precedence markers of any ingested writes.
62	pub fn tick(
63	    id: &str,
64	    project_id: &str,
65	    hint: ParentHint<'_>,
66	    drop_dir: &Path,
67	    now_ms: u64,
68	    cfg: &PulseConfig,
69	) -> std::io::Result<TickOutcome> {
70	    let ingested = ingest_drops(drop_dir, id, project_id, now_ms, cfg.protection_window_ms)?;
71	    let echo_due = take_echo_gate(id, hint.clone());
72	    let idle = resolve_idle_file(id, hint).exists();
73	    Ok(TickOutcome {
74	        ingested,
75	        echo_due,
76	        idle,
77	    })
78	}
79
80	/// Read-and-clear the echo-gate: returns whether it was armed, removing the
81	/// sentinel so the echo fires exactly once. Clearing a missing gate is a no-op.
82	pub fn take_echo_gate(id: &str, hint: ParentHint<'_>) -> bool {
83	    let gate = resolve_echo_gate_file(id, hint);
84	    if gate.exists() {
85	        // Best-effort clear; if removal fails the next tick re-fires (at-least-
86	        // once degrades safely — a duplicate echo is harmless, a lost one less so).
87	        let _ = std::fs::remove_file(&gate);
88	        true
89	    } else {
90	        false
91	    }
92	}
93
94	#[cfg(test)]
95	mod tests {
96	    use super::*;
97	    use crate::test_support::with_home;
98	    use spt_store::perch::resolve_perch_path;
99
100	    fn arm_gate(id: &str) {
101	        let perch = resolve_perch_path(id, ParentHint::Infer);
102	        std::fs::create_dir_all(&perch).unwrap();
103	        std::fs::write(resolve_echo_gate_file(id, ParentHint::Infer), "").unwrap();
104	    }
105
106	    #[test] // [unit->REQ-SEAM-PSYCHE] an armed echo-gate fires exactly once, then clears.
107	    fn echo_gate_fires_exactly_once() {
108	        with_home(|_| {
109	            let drops = tempfile::tempdir().unwrap();
110	            arm_gate("doyle");
111
112	            let t1 = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())
113	                .expect("tick");
114	            assert!(t1.echo_due, "armed gate should fire");
115
116	            // Second tick: gate cleared, no re-fire.
117	            let t2 = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 2000, &PulseConfig::default())
118	                .expect("tick");
119	            assert!(!t2.echo_due, "gate must clear after one fire");
120	        });
121	    }
122
123	    #[test] // [unit->REQ-SEAM-PSYCHE] a tick polls + ingests the drop dir.
124	    fn tick_ingests_drops() {
125	        with_home(|_| {
126	            let drops = tempfile::tempdir().unwrap();
127	            std::fs::write(drops.path().join("doyle-commune.md"), "brief").unwrap();
128
129	            let t = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())
130	                .expect("tick");
131	            assert_eq!(t.ingested.len(), 1);
132	            assert!(!t.echo_due, "no gate armed");
133	            assert!(!drops.path().join("doyle-commune.md").exists(), "drop consumed");
134	        });
135	    }
136
137	    #[test] // [unit->REQ-SEAM-PSYCHE] idle sentinel surfaces in the tick outcome.
138	    fn tick_reports_idle() {
139	        with_home(|_| {
140	            let drops = tempfile::tempdir().unwrap();
141	            let perch = resolve_perch_path("doyle", ParentHint::Infer);
142	            std::fs::create_dir_all(&perch).unwrap();
143	            std::fs::write(resolve_idle_file("doyle", ParentHint::Infer), "").unwrap();
144
145	            let t = tick("doyle", "proj-x", ParentHint::Infer, drops.path(), 1000, &PulseConfig::default())
146	                .expect("tick");
147	            assert!(t.idle);
148	        });
149	    }
150	}


Now lib.rs exports + daemon lifecycle wiring:


Edit

pub use ingest::{ingest_drops, DropKind, Ingested};

pub use ingest::{ingest_drops, route_slices, route_two_slice, DropKind, Ingested, Tier, TierWrite};


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

//! spt-core live-agent lifecycle executor (M2b).
//!
//! `spt-live` *executes* the lifecycle seams that `spt-runtime` (M2a) only
//! declared and parsed. It is the fifth layer in the acyclic stack
//! (`spt-proto -> spt-store -> spt-msg -> spt-runtime -> spt-live`); the `spt`
//! binary's `api listen` runner hosts it.
//!
//! Unlike `spt-runtime`, `spt-live` is **not** part of the public SDK surface
//! (R-ARCH-2 is proto/runtime/msg) — it is the daemon-bound executor M3
//! consolidates into `spt-daemon`. Keep every seam thin so that move is a
//! re-host, not a rewrite (mirror M2a's seed-file discipline).
//!
//! Subsystems land across M2b (see `M2b-PLAN.md`):
//! - **history** (T1, [`REQ-SEAM-HISTORY`]) — the three `[history]` strategies
//!   feeding the echo-commune + resume briefs.
//! - **spawn-psyche** (T2, `REQ-SEAM-PSYCHE`) — the nested `<id>-psyche` perch.
//! - **echo-commune** (T3) — the cheap-model brief consuming history.
//! - **drop-file ingest** (T4) — single-writer commune/signoff ingest (6.4/6.5).
//! - **pulse** (T5) — the interim heartbeat in the `api listen` runner.
//! - **signoff/boundary** (T6) — the orphan/grace ordering invariants (1.1/3.2/3.3).
//! - **resume** (T7, `REQ-SEAM-RESUME`) — fresh-with-preload + continue-existing.
//
// The interim no-daemon model holds (decision 2026-06-01): the Psyche is a real
// subprocess and the `api listen` process hosts the pulse/ingest loops. M3 moves
// both into the daemon. See `M2b-PLAN.md` §Interim-model note.

pub mod context;
pub mod echo;
pub mod history;
pub mod ingest;
pub mod psyche;
pub mod pulse;
pub mod resume;
pub mod signoff;

#[cfg(test)]
mod test_support;

pub use context::{write_context, WriteOutcome, WriteSource};
pub use echo::{run_echo_commune, stamp_provenance, EchoError, EchoResult};
pub use history::{fetch_history, HistoryError, HistoryRecord};
pub use ingest::{ingest_drops, DropKind, Ingested};
pub use psyche::{compose_psyche_prompt, spawn_psyche, PsycheError, PsycheHandle};
pub use pulse::{tick, PulseConfig, TickOutcome};
pub use resume::{download_psyche_context, resume_psyche, ResumeError, ResumeMode};
pub use signoff::{
    graceful_signoff, sweep_stale_signoff, write_resume_commune, SignoffOutcome,
};

-pub use ingest::{ingest_drops, DropKind, Ingested};

+pub use ingest::{ingest_drops, route_slices, route_two_slice, DropKind, Ingested, Tier, TierWrite};


pub struct BrainLifecycle {
    runtime: ManifestRuntime,
    id: String,
    manifest: Manifest,
    commune_dir: Option<PathBuf>,
    signoff_dir: Option<PathBuf>,
    cfg: DaemonConfig,
}

pub struct BrainLifecycle {
    runtime: ManifestRuntime,
    id: String,
    /// The project this lifecycle's context routes under (two-tier split,
    /// REQ-INST-2): derived once from the watched commune dir's parent — the
    /// drop dir lives inside the project (`<project>/.claude/`), so its parent
    /// is the project root — falling back to the daemon's cwd for a manifest
    /// without watched dirs.
    project_id: String,
    manifest: Manifest,
    commune_dir: Option<PathBuf>,
    signoff_dir: Option<PathBuf>,
    cfg: DaemonConfig,
}


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

pub struct BrainLifecycle {
    runtime: ManifestRuntime,
    id: String,
    manifest: Manifest,
    commune_dir: Option<PathBuf>,
    signoff_dir: Option<PathBuf>,
    cfg: DaemonConfig,
}

pub struct BrainLifecycle {
    runtime: ManifestRuntime,
    id: String,
    /// The project this lifecycle's context routes under (two-tier split,
    /// REQ-INST-2): derived once from the watched commune dir's parent — the
    /// drop dir lives inside the project (`<project>/.claude/`), so its parent
    /// is the project root — falling back to the daemon's cwd for a manifest
    /// without watched dirs.
    project_id: String,
    manifest: Manifest,
    commune_dir: Option<PathBuf>,
    signoff_dir: Option<PathBuf>,
    cfg: DaemonConfig,
}

+    /// The project this lifecycle's context routes under (two-tier split,

+    /// REQ-INST-2): derived once from the watched commune dir's parent — the

+    /// drop dir lives inside the project (`<project>/.claude/`), so its parent

+    /// is the project root — falling back to the daemon's cwd for a manifest


    pub fn with_config(manifest: &Manifest, id: &str, cfg: DaemonConfig) -> Option<BrainLifecycle> {
        manifest.session.psyche_init.as_ref()?;
        Some(BrainLifecycle {
            runtime: ManifestRuntime::new(manifest.clone()),
            id: id.to_string(),
            commune_dir: manifest.session.commune_dir.as_ref().map(PathBuf::from),
            signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),
            manifest: manifest.clone(),
            cfg,
        })
    }

    pub fn with_config(manifest: &Manifest, id: &str, cfg: DaemonConfig) -> Option<BrainLifecycle> {
        manifest.session.psyche_init.as_ref()?;
        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);
        // [impl->REQ-INST-2] project identity for the two-tier context split:
        // the commune drop dir's parent is the project root; cwd is the
        // fallback for a manifest without watched dirs.
        let project_anchor = commune_dir
            .as_ref()
            .and_then(|d| d.parent().map(Path::to_path_buf))
            .or_else(|| std::env::current_dir().ok())
            .unwrap_or_else(|| PathBuf::from("."));
        let project_id = spt_store::project::project_id_for_dir(&project_anchor);
        Some(BrainLifecycle {
            runtime: ManifestRuntime::new(manifest.clone()),
            id: id.to_string(),
            project_id,
            commune_dir,
            signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),
            manifest: manifest.clone(),
            cfg,
        })
    }


    pub fn with_config(manifest: &Manifest, id: &str, cfg: DaemonConfig) -> Option<BrainLifecycle> {
        manifest.session.psyche_init.as_ref()?;
        Some(BrainLifecycle {
            runtime: ManifestRuntime::new(manifest.clone()),
            id: id.to_string(),
            commune_dir: manifest.session.commune_dir.as_ref().map(PathBuf::from),
            signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),
            manifest: manifest.clone(),
            cfg,
        })
    }

    pub fn with_config(manifest: &Manifest, id: &str, cfg: DaemonConfig) -> Option<BrainLifecycle> {
        manifest.session.psyche_init.as_ref()?;
        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);
        // [impl->REQ-INST-2] project identity for the two-tier context split:
        // the commune drop dir's parent is the project root; cwd is the
        // fallback for a manifest without watched dirs.
        let project_anchor = commune_dir
            .as_ref()
            .and_then(|d| d.parent().map(Path::to_path_buf))
            .or_else(|| std::env::current_dir().ok())
            .unwrap_or_else(|| PathBuf::from("."));
        let project_id = spt_store::project::project_id_for_dir(&project_anchor);
        Some(BrainLifecycle {
            runtime: ManifestRuntime::new(manifest.clone()),
            id: id.to_string(),
            project_id,
            commune_dir,
            signoff_dir: manifest.session.signoff_dir.as_ref().map(PathBuf::from),
            manifest: manifest.clone(),
            cfg,
        })
    }

/// testable seam). `None` if the manifest declares no Psyche.

+        let commune_dir = manifest.session.commune_dir.as_ref().map(PathBuf::from);

+        // [impl->REQ-INST-2] project identity for the two-tier context split:

+        // the commune drop dir's parent is the project root; cwd is the

+        // fallback for a manifest without watched dirs.

+        let project_anchor = commune_dir

+            .and_then(|d| d.parent().map(Path::to_path_buf))

+            .or_else(|| std::env::current_dir().ok())

+            .unwrap_or_else(|| PathBuf::from("."));

+        let project_id = spt_store::project::project_id_for_dir(&project_anchor);

-            commune_dir: manifest.session.commune_dir.as_ref().map(PathBuf::from),


        for dir in self.drop_dirs() {
            let mut got =
                ingest_drops(&dir, &self.id, ParentHint::Infer, now, self.cfg.protection_window_ms)
                    .map_err(|e| e.to_string())?;
            ingested.append(&mut got);
        }

        for dir in self.drop_dirs() {
            let mut got =
                ingest_drops(&dir, &self.id, &self.project_id, now, self.cfg.protection_window_ms)
                    .map_err(|e| e.to_string())?;
            ingested.append(&mut got);
        }


        for dir in self.drop_dirs() {
            let mut got =
                ingest_drops(&dir, &self.id, ParentHint::Infer, now, self.cfg.protection_window_ms)
                    .map_err(|e| e.to_string())?;
            ingested.append(&mut got);
        }

        for dir in self.drop_dirs() {
            let mut got =
                ingest_drops(&dir, &self.id, &self.project_id, now, self.cfg.protection_window_ms)
                    .map_err(|e| e.to_string())?;
            ingested.append(&mut got);
        }

-                ingest_drops(&dir, &self.id, ParentHint::Infer, now, self.cfg.protection_window_ms)

+                ingest_drops(&dir, &self.id, &self.project_id, now, self.cfg.protection_window_ms)


19:use std::path::PathBuf;

19:use std::path::PathBuf;


use std::time::{Duration, SystemTime, UNIX_EPOCH};


Now `cmd_rename` context arm:


fn cmd_rename(old_id: &str, new_id: &str) -> i32 {
    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {
        eprintln!("ERROR: invalid new id: {e}");
        return 2;
    }
    match spt_store::rename::rename_perch_in(&perch::owlery_dir(), old_id, new_id) {
        Ok(rippled) => {
            for id in &rippled {
                println!("RENAMED:{id}");
            }
            eprintln!("renamed {old_id} -> {new_id} ({} perch(es))", rippled.len());
            0
        }
        Err(e) => {
            eprintln!("ERROR: {e}");
            1
        }
    }
}

fn cmd_rename(old_id: &str, new_id: &str) -> i32 {
    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {
        eprintln!("ERROR: invalid new id: {e}");
        return 2;
    }
    // Context-store arm FIRST (the a-<id>/p- branch ripple, D6a): it has its
    // own collision check (an existing a-<new> mind refuses — never merge two
    // minds), and refusing here leaves the perch tree untouched.
    // [impl->REQ-INST-11]
    if let Ok(cs) = spt_store::contextstore::ContextStore::open_or_init() {
        if let Err(e) = cs.rename_endpoint(old_id, new_id) {
            eprintln!("ERROR: context rename: {e}");
            return 1;
        }
    }
    match spt_store::rename::rename_perch_in(&perch::owlery_dir(), old_id, new_id) {
        Ok(rippled) => {
            for id in &rippled {
                println!("RENAMED:{id}");
            }
            eprintln!("renamed {old_id} -> {new_id} ({} perch(es))", rippled.len());
            0
        }
        Err(e) => {
            eprintln!("ERROR: {e}");
            1
        }
    }
}


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

fn cmd_rename(old_id: &str, new_id: &str) -> i32 {
    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {
        eprintln!("ERROR: invalid new id: {e}");
        return 2;
    }
    match spt_store::rename::rename_perch_in(&perch::owlery_dir(), old_id, new_id) {
        Ok(rippled) => {
            for id in &rippled {
                println!("RENAMED:{id}");
            }
            eprintln!("renamed {old_id} -> {new_id} ({} perch(es))", rippled.len());
            0
        }
        Err(e) => {
            eprintln!("ERROR: {e}");
            1
        }
    }
}

fn cmd_rename(old_id: &str, new_id: &str) -> i32 {
    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {
        eprintln!("ERROR: invalid new id: {e}");
        return 2;
    }
    // Context-store arm FIRST (the a-<id>/p- branch ripple, D6a): it has its
    // own collision check (an existing a-<new> mind refuses — never merge two
    // minds), and refusing here leaves the perch tree untouched.
    // [impl->REQ-INST-11]
    if let Ok(cs) = spt_store::contextstore::ContextStore::open_or_init() {
        if let Err(e) = cs.rename_endpoint(old_id, new_id) {
            eprintln!("ERROR: context rename: {e}");
            return 1;
        }
    }
    match spt_store::rename::rename_perch_in(&perch::owlery_dir(), old_id, new_id) {
        Ok(rippled) => {
            for id in &rippled {
                println!("RENAMED:{id}");
            }
            eprintln!("renamed {old_id} -> {new_id} ({} perch(es))", rippled.len());
            0
        }
        Err(e) => {
            eprintln!("ERROR: {e}");
            1
        }
    }
}

+    // Context-store arm FIRST (the a-<id>/p- branch ripple, D6a): it has its

+    // own collision check (an existing a-<new> mind refuses — never merge two

+    // minds), and refusing here leaves the perch tree untouched.

+    if let Ok(cs) = spt_store::contextstore::ContextStore::open_or_init() {

+        if let Err(e) = cs.rename_endpoint(old_id, new_id) {

+            eprintln!("ERROR: context rename: {e}");


    // Context-store arm FIRST (the a-<id>/p- branch ripple, D6a): it has its
    // own collision check (an existing a-<new> mind refuses — never merge two
    // minds), and refusing here leaves the perch tree untouched.
    // [impl->REQ-INST-11]
    if let Ok(cs) = spt_store::contextstore::ContextStore::open_or_init() {
        if let Err(e) = cs.rename_endpoint(old_id, new_id) {
            eprintln!("ERROR: context rename: {e}");
            return 1;
        }
    }
    match spt_store::rename::rename_perch_in(&perch::owlery_dir(), old_id, new_id) {
        Ok(rippled) => {
            for id in &rippled {
                println!("RENAMED:{id}");
            }
            eprintln!("renamed {old_id} -> {new_id} ({} perch(es))", rippled.len());
            0
        }
        Err(e) => {
            eprintln!("ERROR: {e}");
            1
        }
    }
}

    // Context-store collision pre-check (an existing a-<new> mind refuses —
    // never merge two minds) BEFORE the perch arm mutates anything; the actual
    // branch ripple runs after the perch arm's own refusals (live/collision)
    // have passed, so a refusal on either side leaves the other untouched.
    // [impl->REQ-INST-11]
    let cs = spt_store::contextstore::ContextStore::open_or_init().ok();
    if let Some(cs) = &cs {
        match cs.branch_store().has_branch(&spt_store::contextstore::agent_branch(new_id)) {
            Ok(true) => {
                eprintln!("ERROR: context branch already exists: a-{new_id}");
                return 1;
            }
            Ok(false) => {}
            Err(e) => {
                eprintln!("ERROR: context store: {e}");
                return 1;
            }
        }
    }
    match spt_store::rename::rename_perch_in(&perch::owlery_dir(), old_id, new_id) {
        Ok(rippled) => {
            if let Some(cs) = &cs {
                // The a-/p- branch ripple (D6a). The collision was pre-checked;
                // a residual failure here is surfaced with the recovery hint.
                if let Err(e) = cs.rename_endpoint(old_id, new_id) {
                    eprintln!(
                        "ERROR: perches renamed but context rename failed: {e}\n\
                         recover: rerun `spt rename {old_id} {new_id}` after fixing the cause \
                         (perch arm will report NotFound and the context arm will retry)"
                    );
                    return 1;
                }
            }
            for id in &rippled {
                println!("RENAMED:{id}");
            }
            eprintln!("renamed {old_id} -> {new_id} ({} perch(es))", rippled.len());
            0
        }
        Err(e) => {
            eprintln!("ERROR: {e}");
            1
        }
    }
}