            protection_window_ms: file.protection_window_ms.unwrap_or(d.protection_window_ms),
            crash_grace: file
                .crash_grace_ms
                .map(Duration::from_millis)
                .unwrap_or(d.crash_grace),
        }
    }

//! Durable daemon configuration (B3) — the configurable pulse period that
//! replaces the interim 5s constant.
//!
//! In the no-daemon interim model the pulse cadence was a hard-coded
//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)
//! folds the pulse loop into the brain as a *scheduled* in-process loop whose
//! period is **durable and configurable**, sourced from a small JSON file under
//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when
//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing
//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence
//! from config, never a constant.
// [impl->REQ-DAEMON-1]

use std::path::{Path, PathBuf};
use std::time::Duration;

use serde::{Deserialize, Serialize};

/// The interim default pulse period carried forward as the config default. The
/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may
/// override it per machine.
pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);

/// Default drop-file precedence protection window (epoch-ms), mirroring the
/// interim `spt_live` guard. Surfaced here so the one config source owns it.
pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;

/// Default orphan-watch grace window (B7): how long the supervised-crash teardown
/// waits after observing a hosted Self's harness owner gone before it rechecks
/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery
/// — a binary handoff relaunch or a brief stale poll — without delaying a real
/// teardown for long. Configurable per machine via `crash_grace_ms`.
pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);

/// The on-disk shape (`<spt_home>/daemon.json`). Every field is optional so a
/// partial file still loads (forward/back-compat) — a missing field takes the
/// default. Stored as plain integers (ms) to keep the file human-editable.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct DaemonConfigFile {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pulse_period_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    protection_window_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    crash_grace_ms: Option<u64>,
}

/// Resolved daemon configuration. The brain's scheduled loops read their cadence
/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DaemonConfig {
    /// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).
    pub pulse_period: Duration,
    /// The drop-file precedence protection window in epoch-ms (6.5).
    pub protection_window_ms: u64,
    /// The orphan-watch grace window (B7): the recovery wait before a
    /// supervised-crash teardown rechecks liveness and signs off (1.1).
    pub crash_grace: Duration,
}

impl Default for DaemonConfig {
    fn default() -> Self {
        DaemonConfig {
            pulse_period: DEFAULT_PULSE_PERIOD,
            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,
            crash_grace: DEFAULT_CRASH_GRACE,
        }
    }
}

impl DaemonConfig {
    /// The canonical config path: `<spt_home>/daemon.json`.
    pub fn default_path() -> PathBuf {
        spt_store::perch::spt_home().join("daemon.json")
    }

    /// Load the daemon config from the canonical path, falling back to the
    /// default if the file is absent or unreadable.
    pub fn load() -> Self {
        Self::load_from(&Self::default_path())
    }

    /// Load from an explicit path. A missing file → default; a present-but-partial
    /// file fills only the named fields and defaults the rest; a malformed file
    /// degrades to the default (a broken config must never wedge the daemon).
    pub fn load_from(path: &Path) -> Self {
        let Ok(raw) = std::fs::read_to_string(path) else {
            return Self::default();
        };
        let file: DaemonConfigFile = match serde_json::from_str(&raw) {
            Ok(f) => f,
            Err(_) => return Self::default(),
        };
        Self::from_file(file)
    }

    fn from_file(file: DaemonConfigFile) -> Self {
        let d = DaemonConfig::default();
        DaemonConfig {
            pulse_period: file
                .pulse_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.pulse_period),
            protection_window_ms: file.protection_window_ms.unwrap_or(d.protection_window_ms),
        }
    }

    /// Persist this config to an explicit path (for `spt` to write the knob).
    pub fn save_to(&self, path: &Path) -> std::io::Result<()> {
        let file = DaemonConfigFile {
            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),
            protection_window_ms: Some(self.protection_window_ms),
        };
        let json = serde_json::to_string_pretty(&file)
            .map_err(|e| std::io::Error::other(e.to_string()))?;
        std::fs::write(path, json)
    }
}

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

    #[test] // [unit->REQ-DAEMON-1] an absent config file yields the default period.
    fn missing_file_is_default() {
        let dir = tempfile::tempdir().unwrap();
        let cfg = DaemonConfig::load_from(&dir.path().join("nope.json"));
        assert_eq!(cfg, DaemonConfig::default());
        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);
    }

    #[test] // [unit->REQ-DAEMON-1] the pulse period is config-driven, not a constant.
    fn pulse_period_is_config_driven() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, r#"{"pulse_period_ms": 250}"#).unwrap();

        let cfg = DaemonConfig::load_from(&path);
        assert_eq!(cfg.pulse_period, Duration::from_millis(250));
        assert_ne!(cfg.pulse_period, DEFAULT_PULSE_PERIOD, "must override the default");
        // The unspecified field keeps its default.
        assert_eq!(cfg.protection_window_ms, DEFAULT_PROTECTION_WINDOW_MS);
    }

    #[test] // [unit->REQ-DAEMON-1] a malformed config degrades to default, never wedges.
    fn malformed_file_degrades_to_default() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, "not json {{{").unwrap();
        assert_eq!(DaemonConfig::load_from(&path), DaemonConfig::default());
    }

    #[test] // [unit->REQ-DAEMON-1] save then load round-trips the configured period.
    fn save_load_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        let cfg = DaemonConfig { pulse_period: Duration::from_millis(750), protection_window_ms: 99 };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }
}

+                .map(Duration::from_millis)

+                .unwrap_or(d.crash_grace),


Edit

        let file = DaemonConfigFile {
            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),
            protection_window_ms: Some(self.protection_window_ms),
        };

        let file = DaemonConfigFile {
            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),
            protection_window_ms: Some(self.protection_window_ms),
            crash_grace_ms: Some(self.crash_grace.as_millis() as u64),
        };


NOTE FROM THE USER: proceed with next steps.


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

        let file = DaemonConfigFile {
            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),
            protection_window_ms: Some(self.protection_window_ms),
            crash_grace_ms: Some(self.crash_grace.as_millis() as u64),
        };

//! Durable daemon configuration (B3) — the configurable pulse period that
//! replaces the interim 5s constant.
//!
//! In the no-daemon interim model the pulse cadence was a hard-coded
//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)
//! folds the pulse loop into the brain as a *scheduled* in-process loop whose
//! period is **durable and configurable**, sourced from a small JSON file under
//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when
//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing
//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence
//! from config, never a constant.
// [impl->REQ-DAEMON-1]

use std::path::{Path, PathBuf};
use std::time::Duration;

use serde::{Deserialize, Serialize};

/// The interim default pulse period carried forward as the config default. The
/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may
/// override it per machine.
pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);

/// Default drop-file precedence protection window (epoch-ms), mirroring the
/// interim `spt_live` guard. Surfaced here so the one config source owns it.
pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;

/// Default orphan-watch grace window (B7): how long the supervised-crash teardown
/// waits after observing a hosted Self's harness owner gone before it rechecks
/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery
/// — a binary handoff relaunch or a brief stale poll — without delaying a real
/// teardown for long. Configurable per machine via `crash_grace_ms`.
pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);

/// The on-disk shape (`<spt_home>/daemon.json`). Every field is optional so a
/// partial file still loads (forward/back-compat) — a missing field takes the
/// default. Stored as plain integers (ms) to keep the file human-editable.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct DaemonConfigFile {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pulse_period_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    protection_window_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    crash_grace_ms: Option<u64>,
}

/// Resolved daemon configuration. The brain's scheduled loops read their cadence
/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DaemonConfig {
    /// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).
    pub pulse_period: Duration,
    /// The drop-file precedence protection window in epoch-ms (6.5).
    pub protection_window_ms: u64,
    /// The orphan-watch grace window (B7): the recovery wait before a
    /// supervised-crash teardown rechecks liveness and signs off (1.1).
    pub crash_grace: Duration,
}

impl Default for DaemonConfig {
    fn default() -> Self {
        DaemonConfig {
            pulse_period: DEFAULT_PULSE_PERIOD,
            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,
            crash_grace: DEFAULT_CRASH_GRACE,
        }
    }
}

impl DaemonConfig {
    /// The canonical config path: `<spt_home>/daemon.json`.
    pub fn default_path() -> PathBuf {
        spt_store::perch::spt_home().join("daemon.json")
    }

    /// Load the daemon config from the canonical path, falling back to the
    /// default if the file is absent or unreadable.
    pub fn load() -> Self {
        Self::load_from(&Self::default_path())
    }

    /// Load from an explicit path. A missing file → default; a present-but-partial
    /// file fills only the named fields and defaults the rest; a malformed file
    /// degrades to the default (a broken config must never wedge the daemon).
    pub fn load_from(path: &Path) -> Self {
        let Ok(raw) = std::fs::read_to_string(path) else {
            return Self::default();
        };
        let file: DaemonConfigFile = match serde_json::from_str(&raw) {
            Ok(f) => f,
            Err(_) => return Self::default(),
        };
        Self::from_file(file)
    }

    fn from_file(file: DaemonConfigFile) -> Self {
        let d = DaemonConfig::default();
        DaemonConfig {
            pulse_period: file
                .pulse_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.pulse_period),
            protection_window_ms: file.protection_window_ms.unwrap_or(d.protection_window_ms),
            crash_grace: file
                .crash_grace_ms
                .map(Duration::from_millis)
                .unwrap_or(d.crash_grace),
        }
    }

    /// Persist this config to an explicit path (for `spt` to write the knob).
    pub fn save_to(&self, path: &Path) -> std::io::Result<()> {
        let file = DaemonConfigFile {
            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),
            protection_window_ms: Some(self.protection_window_ms),
        };
        let json = serde_json::to_string_pretty(&file)
            .map_err(|e| std::io::Error::other(e.to_string()))?;
        std::fs::write(path, json)
    }
}

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

    #[test] // [unit->REQ-DAEMON-1] an absent config file yields the default period.
    fn missing_file_is_default() {
        let dir = tempfile::tempdir().unwrap();
        let cfg = DaemonConfig::load_from(&dir.path().join("nope.json"));
        assert_eq!(cfg, DaemonConfig::default());
        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);
    }

    #[test] // [unit->REQ-DAEMON-1] the pulse period is config-driven, not a constant.
    fn pulse_period_is_config_driven() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, r#"{"pulse_period_ms": 250}"#).unwrap();

        let cfg = DaemonConfig::load_from(&path);
        assert_eq!(cfg.pulse_period, Duration::from_millis(250));
        assert_ne!(cfg.pulse_period, DEFAULT_PULSE_PERIOD, "must override the default");
        // The unspecified field keeps its default.
        assert_eq!(cfg.protection_window_ms, DEFAULT_PROTECTION_WINDOW_MS);
    }

    #[test] // [unit->REQ-DAEMON-1] a malformed config degrades to default, never wedges.
    fn malformed_file_degrades_to_default() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, "not json {{{").unwrap();
        assert_eq!(DaemonConfig::load_from(&path), DaemonConfig::default());
    }

    #[test] // [unit->REQ-DAEMON-1] save then load round-trips the configured period.
    fn save_load_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        let cfg = DaemonConfig { pulse_period: Duration::from_millis(750), protection_window_ms: 99 };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }
}

+            crash_grace_ms: Some(self.crash_grace.as_millis() as u64),


Fix the roundtrip test struct literal + add a crash_grace config test.


        let cfg = DaemonConfig { pulse_period: Duration::from_millis(750), protection_window_ms: 99 };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }
}

        let cfg = DaemonConfig {
            pulse_period: Duration::from_millis(750),
            protection_window_ms: 99,
            crash_grace: Duration::from_millis(1234),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

    #[test] // [unit->REQ-DAEMON-4] the orphan-watch grace is config-driven, default otherwise.
    fn crash_grace_is_config_driven() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, r#"{"crash_grace_ms": 500}"#).unwrap();

        let cfg = DaemonConfig::load_from(&path);
        assert_eq!(cfg.crash_grace, Duration::from_millis(500));
        assert_ne!(cfg.crash_grace, DEFAULT_CRASH_GRACE, "must override the default");
        // Unspecified fields keep their defaults.
        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);
        // An absent file yields the default grace.
        assert_eq!(
            DaemonConfig::load_from(&dir.path().join("nope.json")).crash_grace,
            DEFAULT_CRASH_GRACE
        );
    }
}


        let cfg = DaemonConfig {
            pulse_period: Duration::from_millis(750),
            protection_window_ms: 99,
            crash_grace: Duration::from_millis(1234),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

    #[test] // [unit->REQ-DAEMON-4] the orphan-watch grace is config-driven, default otherwise.
    fn crash_grace_is_config_driven() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, r#"{"crash_grace_ms": 500}"#).unwrap();

        let cfg = DaemonConfig::load_from(&path);
        assert_eq!(cfg.crash_grace, Duration::from_millis(500));
        assert_ne!(cfg.crash_grace, DEFAULT_CRASH_GRACE, "must override the default");
        // Unspecified fields keep their defaults.
        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);
        // An absent file yields the default grace.
        assert_eq!(
            DaemonConfig::load_from(&dir.path().join("nope.json")).crash_grace,
            DEFAULT_CRASH_GRACE
        );
    }
}

//! Durable daemon configuration (B3) — the configurable pulse period that
//! replaces the interim 5s constant.
//!
//! In the no-daemon interim model the pulse cadence was a hard-coded
//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)
//! folds the pulse loop into the brain as a *scheduled* in-process loop whose
//! period is **durable and configurable**, sourced from a small JSON file under
//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when
//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing
//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence
//! from config, never a constant.
// [impl->REQ-DAEMON-1]

use std::path::{Path, PathBuf};
use std::time::Duration;

use serde::{Deserialize, Serialize};

/// The interim default pulse period carried forward as the config default. The
/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may
/// override it per machine.
pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);

/// Default drop-file precedence protection window (epoch-ms), mirroring the
/// interim `spt_live` guard. Surfaced here so the one config source owns it.
pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;

/// Default orphan-watch grace window (B7): how long the supervised-crash teardown
/// waits after observing a hosted Self's harness owner gone before it rechecks
/// liveness and signs off (KNOWN-HAZARDS 1.1). Sized to cover a transient recovery
/// — a binary handoff relaunch or a brief stale poll — without delaying a real
/// teardown for long. Configurable per machine via `crash_grace_ms`.
pub const DEFAULT_CRASH_GRACE: Duration = Duration::from_secs(2);

/// The on-disk shape (`<spt_home>/daemon.json`). Every field is optional so a
/// partial file still loads (forward/back-compat) — a missing field takes the
/// default. Stored as plain integers (ms) to keep the file human-editable.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct DaemonConfigFile {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pulse_period_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    protection_window_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    crash_grace_ms: Option<u64>,
}

/// Resolved daemon configuration. The brain's scheduled loops read their cadence
/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DaemonConfig {
    /// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).
    pub pulse_period: Duration,
    /// The drop-file precedence protection window in epoch-ms (6.5).
    pub protection_window_ms: u64,
    /// The orphan-watch grace window (B7): the recovery wait before a
    /// supervised-crash teardown rechecks liveness and signs off (1.1).
    pub crash_grace: Duration,
}

impl Default for DaemonConfig {
    fn default() -> Self {
        DaemonConfig {
            pulse_period: DEFAULT_PULSE_PERIOD,
            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,
            crash_grace: DEFAULT_CRASH_GRACE,
        }
    }
}

impl DaemonConfig {
    /// The canonical config path: `<spt_home>/daemon.json`.
    pub fn default_path() -> PathBuf {
        spt_store::perch::spt_home().join("daemon.json")
    }

    /// Load the daemon config from the canonical path, falling back to the
    /// default if the file is absent or unreadable.
    pub fn load() -> Self {
        Self::load_from(&Self::default_path())
    }

    /// Load from an explicit path. A missing file → default; a present-but-partial
    /// file fills only the named fields and defaults the rest; a malformed file
    /// degrades to the default (a broken config must never wedge the daemon).
    pub fn load_from(path: &Path) -> Self {
        let Ok(raw) = std::fs::read_to_string(path) else {
            return Self::default();
        };
        let file: DaemonConfigFile = match serde_json::from_str(&raw) {
            Ok(f) => f,
            Err(_) => return Self::default(),
        };
        Self::from_file(file)
    }

    fn from_file(file: DaemonConfigFile) -> Self {
        let d = DaemonConfig::default();
        DaemonConfig {
            pulse_period: file
                .pulse_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.pulse_period),
            protection_window_ms: file.protection_window_ms.unwrap_or(d.protection_window_ms),
            crash_grace: file
                .crash_grace_ms
                .map(Duration::from_millis)
                .unwrap_or(d.crash_grace),
        }
    }

    /// Persist this config to an explicit path (for `spt` to write the knob).
    pub fn save_to(&self, path: &Path) -> std::io::Result<()> {
        let file = DaemonConfigFile {
            pulse_period_ms: Some(self.pulse_period.as_millis() as u64),
            protection_window_ms: Some(self.protection_window_ms),
            crash_grace_ms: Some(self.crash_grace.as_millis() as u64),
        };
        let json = serde_json::to_string_pretty(&file)
            .map_err(|e| std::io::Error::other(e.to_string()))?;
        std::fs::write(path, json)
    }
}

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

    #[test] // [unit->REQ-DAEMON-1] an absent config file yields the default period.
    fn missing_file_is_default() {
        let dir = tempfile::tempdir().unwrap();
        let cfg = DaemonConfig::load_from(&dir.path().join("nope.json"));
        assert_eq!(cfg, DaemonConfig::default());
        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);
    }

    #[test] // [unit->REQ-DAEMON-1] the pulse period is config-driven, not a constant.
    fn pulse_period_is_config_driven() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, r#"{"pulse_period_ms": 250}"#).unwrap();

        let cfg = DaemonConfig::load_from(&path);
        assert_eq!(cfg.pulse_period, Duration::from_millis(250));
        assert_ne!(cfg.pulse_period, DEFAULT_PULSE_PERIOD, "must override the default");
        // The unspecified field keeps its default.
        assert_eq!(cfg.protection_window_ms, DEFAULT_PROTECTION_WINDOW_MS);
    }

    #[test] // [unit->REQ-DAEMON-1] a malformed config degrades to default, never wedges.
    fn malformed_file_degrades_to_default() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, "not json {{{").unwrap();
        assert_eq!(DaemonConfig::load_from(&path), DaemonConfig::default());
    }

    #[test] // [unit->REQ-DAEMON-1] save then load round-trips the configured period.
    fn save_load_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        let cfg = DaemonConfig { pulse_period: Duration::from_millis(750), protection_window_ms: 99 };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }
}

-        let cfg = DaemonConfig { pulse_period: Duration::from_millis(750), protection_window_ms: 99 };

+            pulse_period: Duration::from_millis(750),

+            crash_grace: Duration::from_millis(1234),

+    #[test] // [unit->REQ-DAEMON-4] the orphan-watch grace is config-driven, default otherwise.

+    fn crash_grace_is_config_driven() {

+        let dir = tempfile::tempdir().unwrap();

+        let path = dir.path().join("daemon.json");

+        std::fs::write(&path, r#"{"crash_grace_ms": 500}"#).unwrap();

+        let cfg = DaemonConfig::load_from(&path);

+        assert_eq!(cfg.crash_grace, Duration::from_millis(500));

+        assert_ne!(cfg.crash_grace, DEFAULT_CRASH_GRACE, "must override the default");

+        // Unspecified fields keep their defaults.

+        assert_eq!(cfg.pulse_period, DEFAULT_PULSE_PERIOD);

+        // An absent file yields the default grace.

+            DaemonConfig::load_from(&dir.path().join("nope.json")).crash_grace,


Task 1 done. Task 2 — lifecycle refactor + orphan-watch.


use spt_runtime::{Manifest, ManifestRuntime};
use spt_store::perch::{self, ParentHint};

use spt_runtime::{Manifest, ManifestRuntime};
use spt_store::perch::{self, ParentHint};
use spt_store::proc::is_process_alive;


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)

use spt_runtime::{Manifest, ManifestRuntime};
use spt_store::perch::{self, ParentHint};
use spt_store::proc::is_process_alive;


config: add crash_grace knob

Add crash_grace Duration to DaemonConfig + crash_grace_ms to file struct + DEFAULT_CRASH_GRACE const. The grace window (hazard 1.1) for the orphan-watch recheck.

lifecycle: supervised-crash signoff + orphan-watch loop

Refactor graceful_signoff to share wiring; add supervised_crash_signoff (real grace + real recheck), harness_owner_gone (parent_pid probe, 2.1), run_orphan_watch (poll owner pid, sweep-on-start 3.2, fire crash signoff on death). Tests: same ordering as graceful; recovered Self spared.

Activate REQ-DAEMON-4 + tag evidence

Set REQ-DAEMON-4 required_stages=["impl","unit"] in traceable-reqs.toml with B7 note; tag impl/unit evidence in lifecycle.rs.

Gate green + commit

cargo test --workspace, clippy -D warnings, traceable-reqs check. Then commit with Co-Authored-By trailer.


Now refactor the signoff method + add orphan-watch. Replace the `graceful_signoff` method block.


    /// Execute the graceful signoff (T6): echo-commune BEFORE teardown (3.3),
    /// grace recheck before any signoff write (1.1). The interim graceful path
    /// uses zero grace + always-gone recheck (an intentional shutdown); the
    /// daemon's orphan-watch (B7) supplies the real grace/liveness recheck.
    pub fn graceful_signoff(&self, session_id: Option<&str>) -> Result<SignoffOutcome, String> {
        let mut echo_err: Option<String> = None;
        let mut teardown_err: Option<String> = None;
        let outcome = graceful_signoff::<std::convert::Infallible>(
            session_id,
            Duration::ZERO,
            || {
                if let Err(e) = self.fire_echo(session_id) {
                    echo_err = Some(e);
                }
                Ok(())
            },
            || true, // graceful, intentional shutdown — the Self is leaving on purpose
            || {
                let ready = perch::resolve_ready_file(&self.id, ParentHint::Infer);
                if let Err(e) = std::fs::remove_file(&ready) {
                    if e.kind() != std::io::ErrorKind::NotFound {
                        teardown_err = Some(e.to_string());
                    }
                }
                Ok(())
            },
        )
        .unwrap_or(SignoffOutcome { echo_ran: false, signed_off: false });
        if let Some(e) = echo_err {
            return Err(format!("echo failed during signoff: {e}"));
        }
        if let Some(e) = teardown_err {
            return Err(format!("teardown failed during signoff: {e}"));
        }
        Ok(outcome)
    }
}

    /// Execute the graceful signoff (T6): echo-commune BEFORE teardown (3.3),
    /// grace recheck before any signoff write (1.1). The graceful path is an
    /// **intentional** shutdown, so it uses zero grace + an always-gone recheck —
    /// the Self is leaving on purpose, there is nothing to recover. The
    /// supervised-crash path ([`supervised_crash_signoff`](Self::supervised_crash_signoff))
    /// supplies the real grace + liveness recheck for an *un*intentional death.
    pub fn graceful_signoff(&self, session_id: Option<&str>) -> Result<SignoffOutcome, String> {
        self.signoff_with(session_id, Duration::ZERO, || true)
    }

    /// Execute the **supervised-crash** signoff (B7, orphan-watch) — the
    /// counterpart of [`graceful_signoff`](Self::graceful_signoff) for an
    /// *unexpected* harness death. It runs the identical load-bearing ordering —
    /// echo-commune (3.3) → grace wait (1.1) → liveness recheck → teardown — but
    /// with the **real** grace window from config and the **real** recheck
    /// `still_gone`. A Self that recovered across the grace window (a binary
    /// handoff relaunch, a brief stale poll) makes `still_gone` false and is
    /// **spared** — the phantom-INIT_SIGNOFF bug (1.1).
    ///
    /// `still_gone` is the post-grace liveness verdict (true ⇒ the harness owner
    /// is still gone ⇒ tear down). [`run_orphan_watch`](Self::run_orphan_watch)
    /// supplies the production recheck ([`harness_owner_gone`](Self::harness_owner_gone));
    /// it is injected here so the ordering is testable without a real pid.
    // [impl->REQ-DAEMON-4]
    // [impl->REQ-HAZARD-GRACE-BEFORE-SIGNOFF]
    // [impl->REQ-HAZARD-ECHO-BEFORE-SIGNOFF]
    pub fn supervised_crash_signoff(
        &self,
        session_id: Option<&str>,
        still_gone: impl FnOnce() -> bool,
    ) -> Result<SignoffOutcome, String> {
        self.signoff_with(session_id, self.cfg.crash_grace, still_gone)
    }

    /// The shared signoff wiring behind both the graceful and supervised-crash
    /// paths. The only two things that differ between them are exactly the two
    /// the 1.1 hazard turns on — the `grace` window and the `recheck` verdict — so
    /// they are the only parameters; the echo (3.3) and teardown effects are
    /// identical. Keeping one body guarantees the crash path can never drift out
    /// of the echo-before-teardown ordering the graceful path proves.
    fn signoff_with(
        &self,
        session_id: Option<&str>,
        grace: Duration,
        recheck: impl FnOnce() -> bool,
    ) -> Result<SignoffOutcome, String> {
        let mut echo_err: Option<String> = None;
        let mut teardown_err: Option<String> = None;
        let outcome = graceful_signoff::<std::convert::Infallible>(
            session_id,
            grace,
            || {
                if let Err(e) = self.fire_echo(session_id) {
                    echo_err = Some(e);
                }
                Ok(())
            },
            || recheck(),
            || {
                let ready = perch::resolve_ready_file(&self.id, ParentHint::Infer);
                if let Err(e) = std::fs::remove_file(&ready) {
                    if e.kind() != std::io::ErrorKind::NotFound {
                        teardown_err = Some(e.to_string());
                    }
                }
                Ok(())
            },
        )
        .unwrap_or(SignoffOutcome { echo_ran: false, signed_off: false });
        if let Some(e) = echo_err {
            return Err(format!("echo failed during signoff: {e}"));
        }
        if let Some(e) = teardown_err {
            return Err(format!("teardown failed during signoff: {e}"));
        }
        Ok(outcome)
    }

    /// Whether the supervised Self's **harness owner** is gone. Anchors liveness on
    /// the stable harness-session pid (`info.json` `parent_pid`), never an
    /// ephemeral poll pid (KNOWN-HAZARDS 2.1): a recycled ephemeral pid could read
    /// a foreign process as the Self and mask a real death, or flip a live Self
    /// dead. Re-reads the perch fresh so a Self that **rebound** under a new
    /// harness (a recovery) — recording a fresh, live `parent_pid` — reads alive.
    /// A perch with no recorded owner is treated as gone (nothing supervises it).
    // [impl->REQ-DAEMON-4]
    pub fn harness_owner_gone(&self) -> bool {
        let path = perch::resolve_perch_path(&self.id, ParentHint::Infer);
        match spt_store::info::read_info(&path).and_then(|i| i.parent_pid) {
            Some(pid) => !is_process_alive(pid),
            None => true,
        }
    }

    /// Supervise a harness-hosted Self and carry the graceful ordering into the
    /// **supervised-crash** path the always-on daemon now enables (B7). On start it
    /// sweeps any stale signoff sentinel (3.2) so a prior generation's leftover
    /// never reads as a live signoff. It then polls the harness owner pid each
    /// cycle (anchored on `parent_pid`, 2.1); on the first observed death it runs
    /// [`supervised_crash_signoff`](Self::supervised_crash_signoff) with the real
    /// grace + a fresh liveness recheck ([`harness_owner_gone`](Self::harness_owner_gone)),
    /// so a transient recovery across the grace window is spared (1.1).
    ///
    /// Returns the signoff outcome once the watch fires, or `None` if `stop` was
    /// set before any death was observed. `owner_pid` is the supervised harness
    /// pid; the poll cadence reuses the configured `pulse_period` (the same
    /// scheduled-supervision clock as the pulse loop).
    // [impl->REQ-DAEMON-4]
    // [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]
    pub fn run_orphan_watch(
        &self,
        session_id: Option<&str>,
        owner_pid: u32,
        stop: &AtomicBool,
    ) -> Option<Result<SignoffOutcome, String>> {
        // 3.2 — sweep a stale signoff sentinel before supervising, exactly as a
        // listener start does, so a prior crash's sentinel can't self-trigger.
        self.sweep_on_start();
        while !stop.load(Ordering::Relaxed) {
            if !is_process_alive(owner_pid) {
                // Observed gone — run the crash teardown. The recheck re-reads the
                // perch's CURRENT owner, so a Self that rebound across the grace
                // window (binary handoff) is spared (1.1).
                return Some(self.supervised_crash_signoff(session_id, || self.harness_owner_gone()));
            }
            sleep_interruptible(self.cfg.pulse_period, stop);
        }
        None
    }
}


    /// Execute the graceful signoff (T6): echo-commune BEFORE teardown (3.3),
    /// grace recheck before any signoff write (1.1). The interim graceful path
    /// uses zero grace + always-gone recheck (an intentional shutdown); the
    /// daemon's orphan-watch (B7) supplies the real grace/liveness recheck.
    pub fn graceful_signoff(&self, session_id: Option<&str>) -> Result<SignoffOutcome, String> {
        let mut echo_err: Option<String> = None;
        let mut teardown_err: Option<String> = None;
        let outcome = graceful_signoff::<std::convert::Infallible>(
            session_id,
            Duration::ZERO,
            || {
                if let Err(e) = self.fire_echo(session_id) {
                    echo_err = Some(e);
                }
                Ok(())
            },
            || true, // graceful, intentional shutdown — the Self is leaving on purpose
            || {
                let ready = perch::resolve_ready_file(&self.id, ParentHint::Infer);
                if let Err(e) = std::fs::remove_file(&ready) {
                    if e.kind() != std::io::ErrorKind::NotFound {
                        teardown_err = Some(e.to_string());
                    }
                }
                Ok(())
            },
        )
        .unwrap_or(SignoffOutcome { echo_ran: false, signed_off: false });
        if let Some(e) = echo_err {
            return Err(format!("echo failed during signoff: {e}"));
        }
        if let Some(e) = teardown_err {
            return Err(format!("teardown failed during signoff: {e}"));
        }
        Ok(outcome)
    }
}

    /// Execute the graceful signoff (T6): echo-commune BEFORE teardown (3.3),
    /// grace recheck before any signoff write (1.1). The graceful path is an
    /// **intentional** shutdown, so it uses zero grace + an always-gone recheck —
    /// the Self is leaving on purpose, there is nothing to recover. The
    /// supervised-crash path ([`supervised_crash_signoff`](Self::supervised_crash_signoff))
    /// supplies the real grace + liveness recheck for an *un*intentional death.
    pub fn graceful_signoff(&self, session_id: Option<&str>) -> Result<SignoffOutcome, String> {
        self.signoff_with(session_id, Duration::ZERO, || true)
    }

    /// Execute the **supervised-crash** signoff (B7, orphan-watch) — the
    /// counterpart of [`graceful_signoff`](Self::graceful_signoff) for an
    /// *unexpected* harness death. It runs the identical load-bearing ordering —
    /// echo-commune (3.3) → grace wait (1.1) → liveness recheck → teardown — but
    /// with the **real** grace window from config and the **real** recheck
    /// `still_gone`. A Self that recovered across the grace window (a binary
    /// handoff relaunch, a brief stale poll) makes `still_gone` false and is
    /// **spared** — the phantom-INIT_SIGNOFF bug (1.1).
    ///
    /// `still_gone` is the post-grace liveness verdict (true ⇒ the harness owner
    /// is still gone ⇒ tear down). [`run_orphan_watch`](Self::run_orphan_watch)
    /// supplies the production recheck ([`harness_owner_gone`](Self::harness_owner_gone));
    /// it is injected here so the ordering is testable without a real pid.
    // [impl->REQ-DAEMON-4]
    // [impl->REQ-HAZARD-GRACE-BEFORE-SIGNOFF]
    // [impl->REQ-HAZARD-ECHO-BEFORE-SIGNOFF]
    pub fn supervised_crash_signoff(
        &self,
        session_id: Option<&str>,
        still_gone: impl FnOnce() -> bool,
    ) -> Result<SignoffOutcome, String> {
        self.signoff_with(session_id, self.cfg.crash_grace, still_gone)
    }

    /// The shared signoff wiring behind both the graceful and supervised-crash
    /// paths. The only two things that differ between them are exactly the two
    /// the 1.1 hazard turns on — the `grace` window and the `recheck` verdict — so
    /// they are the only parameters; the echo (3.3) and teardown effects are
    /// identical. Keeping one body guarantees the crash path can never drift out
    /// of the echo-before-teardown ordering the graceful path proves.
    fn signoff_with(
        &self,
        session_id: Option<&str>,
        grace: Duration,
        recheck: impl FnOnce() -> bool,
    ) -> Result<SignoffOutcome, String> {
        let mut echo_err: Option<String> = None;
        let mut teardown_err: Option<String> = None;
        let outcome = graceful_signoff::<std::convert::Infallible>(
            session_id,
            grace,
            || {
                if let Err(e) = self.fire_echo(session_id) {
                    echo_err = Some(e);
                }
                Ok(())
            },
            || recheck(),
            || {
                let ready = perch::resolve_ready_file(&self.id, ParentHint::Infer);
                if let Err(e) = std::fs::remove_file(&ready) {
                    if e.kind() != std::io::ErrorKind::NotFound {
                        teardown_err = Some(e.to_string());
                    }
                }
                Ok(())
            },
        )
        .unwrap_or(SignoffOutcome { echo_ran: false, signed_off: false });
        if let Some(e) = echo_err {
            return Err(format!("echo failed during signoff: {e}"));
        }
        if let Some(e) = teardown_err {
            return Err(format!("teardown failed during signoff: {e}"));
        }
        Ok(outcome)
    }

    /// Whether the supervised Self's **harness owner** is gone. Anchors liveness on
    /// the stable harness-session pid (`info.json` `parent_pid`), never an
    /// ephemeral poll pid (KNOWN-HAZARDS 2.1): a recycled ephemeral pid could read
    /// a foreign process as the Self and mask a real death, or flip a live Self
    /// dead. Re-reads the perch fresh so a Self that **rebound** under a new
    /// harness (a recovery) — recording a fresh, live `parent_pid` — reads alive.
    /// A perch with no recorded owner is treated as gone (nothing supervises it).
    // [impl->REQ-DAEMON-4]
    pub fn harness_owner_gone(&self) -> bool {
        let path = perch::resolve_perch_path(&self.id, ParentHint::Infer);
        match spt_store::info::read_info(&path).and_then(|i| i.parent_pid) {
            Some(pid) => !is_process_alive(pid),
            None => true,
        }
    }

    /// Supervise a harness-hosted Self and carry the graceful ordering into the
    /// **supervised-crash** path the always-on daemon now enables (B7). On start it
    /// sweeps any stale signoff sentinel (3.2) so a prior generation's leftover
    /// never reads as a live signoff. It then polls the harness owner pid each
    /// cycle (anchored on `parent_pid`, 2.1); on the first observed death it runs
    /// [`supervised_crash_signoff`](Self::supervised_crash_signoff) with the real
    /// grace + a fresh liveness recheck ([`harness_owner_gone`](Self::harness_owner_gone)),
    /// so a transient recovery across the grace window is spared (1.1).
    ///
    /// Returns the signoff outcome once the watch fires, or `None` if `stop` was
    /// set before any death was observed. `owner_pid` is the supervised harness
    /// pid; the poll cadence reuses the configured `pulse_period` (the same
    /// scheduled-supervision clock as the pulse loop).
    // [impl->REQ-DAEMON-4]
    // [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]
    pub fn run_orphan_watch(
        &self,
        session_id: Option<&str>,
        owner_pid: u32,
        stop: &AtomicBool,
    ) -> Option<Result<SignoffOutcome, String>> {
        // 3.2 — sweep a stale signoff sentinel before supervising, exactly as a
        // listener start does, so a prior crash's sentinel can't self-trigger.
        self.sweep_on_start();
        while !stop.load(Ordering::Relaxed) {
            if !is_process_alive(owner_pid) {
                // Observed gone — run the crash teardown. The recheck re-reads the
                // perch's CURRENT owner, so a Self that rebound across the grace
                // window (binary handoff) is spared (1.1).
                return Some(self.supervised_crash_signoff(session_id, || self.harness_owner_gone()));
            }
            sleep_interruptible(self.cfg.pulse_period, stop);
        }
        None
    }
}

-    /// grace recheck before any signoff write (1.1). The interim graceful path

-    /// uses zero grace + always-gone recheck (an intentional shutdown); the

-    /// daemon's orphan-watch (B7) supplies the real grace/liveness recheck.

+    /// grace recheck before any signoff write (1.1). The graceful path is an

+    /// **intentional** shutdown, so it uses zero grace + an always-gone recheck —

+    /// the Self is leaving on purpose, there is nothing to recover. The

+    /// supervised-crash path ([`supervised_crash_signoff`](Self::supervised_crash_signoff))

+    /// supplies the real grace + liveness recheck for an *un*intentional death.

+        self.signoff_with(session_id, Duration::ZERO, || true)

+    /// Execute the **supervised-crash** signoff (B7, orphan-watch) — the

+    /// counterpart of [`graceful_signoff`](Self::graceful_signoff) for an

+    /// *unexpected* harness death. It runs the identical load-bearing ordering —

+    /// echo-commune (3.3) → grace wait (1.1) → liveness recheck → teardown — but

+    /// with the **real** grace window from config and the **real** recheck

+    /// `still_gone`. A Self that recovered across the grace window (a binary

+    /// handoff relaunch, a brief stale poll) makes `still_gone` false and is

+    /// **spared** — the phantom-INIT_SIGNOFF bug (1.1).

+    /// `still_gone` is the post-grace liveness verdict (true ⇒ the harness owner

+    /// is still gone ⇒ tear down). [`run_orphan_watch`](Self::run_orphan_watch)

+    /// supplies the production recheck ([`harness_owner_gone`](Self::harness_owner_gone));

+    /// it is injected here so the ordering is testable without a real pid.

+    // [impl->REQ-HAZARD-GRACE-BEFORE-SIGNOFF]

+    // [impl->REQ-HAZARD-ECHO-BEFORE-SIGNOFF]

+        still_gone: impl FnOnce() -> bool,

+    ) -> Result<SignoffOutcome, String> {

+        self.signoff_with(session_id, self.cfg.crash_grace, still_gone)

+    /// The shared signoff wiring behind both the graceful and supervised-crash

+    /// paths. The only two things that differ between them are exactly the two

+    /// the 1.1 hazard turns on — the `grace` window and the `recheck` verdict — so

+    /// they are the only parameters; the echo (3.3) and teardown effects are

+    /// identical. Keeping one body guarantees the crash path can never drift out

+    /// of the echo-before-teardown ordering the graceful path proves.

+        recheck: impl FnOnce() -> bool,

let mut echo_err: Option<String> = None;

let mut teardown_err: Option<String> = None;

if let Err(e) = self.fire_echo(session_id) {

-            || true, // graceful, intentional shutdown — the Self is leaving on purpose

if let Err(e) = std::fs::remove_file(&ready) {

+    /// Whether the supervised Self's **harness owner** is gone. Anchors liveness on

+    /// the stable harness-session pid (`info.json` `parent_pid`), never an

+    /// ephemeral poll pid (KNOWN-HAZARDS 2.1): a recycled ephemeral pid could read

+    /// a foreign process as the Self and mask a real death, or flip a live Self

+    /// dead. Re-reads the perch fresh so a Self that **rebound** under a new

+    /// harness (a recovery) — recording a fresh, live `parent_pid` — reads alive.

+    /// A perch with no recorded owner is treated as gone (nothing supervises it).

+    pub fn harness_owner_gone(&self) -> bool {

+        let path = perch::resolve_perch_path(&self.id, ParentHint::Infer);

+        match spt_store::info::read_info(&path).and_then(|i| i.parent_pid) {

+            Some(pid) => !is_process_alive(pid),

+    /// Supervise a harness-hosted Self and carry the graceful ordering into the

+    /// **supervised-crash** path the always-on daemon now enables (B7). On start it

+    /// sweeps any stale signoff sentinel (3.2) so a prior generation's leftover

+    /// never reads as a live signoff. It then polls the harness owner pid each

+    /// cycle (anchored on `parent_pid`, 2.1); on the first observed death it runs

+    /// [`supervised_crash_signoff`](Self::supervised_crash_signoff) with the real

+    /// grace + a fresh liveness recheck ([`harness_owner_gone`](Self::harness_owner_gone)),

+    /// so a transient recovery across the grace window is spared (1.1).

+    /// Returns the signoff outcome once the watch fires, or `None` if `stop` was

+    /// set before any death was observed. `owner_pid` is the supervised harness

+    /// pid; the poll cadence reuses the configured `pulse_period` (the same

+    /// scheduled-supervision clock as the pulse loop).

+    // [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]

+    ) -> Option<Result<SignoffOutcome, String>> {

+        // 3.2 — sweep a stale signoff sentinel before supervising, exactly as a

+        // listener start does, so a prior crash's sentinel can't self-trigger.

+        while !stop.load(Ordering::Relaxed) {

+            if !is_process_alive(owner_pid) {

+                // Observed gone — run the crash teardown. The recheck re-reads the

+                // perch's CURRENT owner, so a Self that rebound across the grace

+                // window (binary handoff) is spared (1.1).

+                return Some(self.supervised_crash_signoff(session_id, || self.harness_owner_gone()));

+            sleep_interruptible(self.cfg.pulse_period, stop);

/// Sleep `period`, waking early if `stop` is set (10ms slices). Keeps the pulse


Check `run_echo_commune` behavior with the mock manifest (does it spawn / error?).


Bash

grep -n 'pub fn run_echo_commune\|pub fn fetch_history\|psyche_init\|echo' crates/spt-live/src/echo.rs | head -20

Inspect run_echo_commune


1://! echo-commune seam (M2b T3) — the cheap-model brief that keeps a Psyche's
4://! spt-core spawns the `[session.echo_commune]` template (a cheap model — the
13://! signal, few/no live edits. Every commune the echo writes is tagged
14://! `Source: echo-commune` so the later memformat pass (deferred) inherits the
18://! The echo child runs with the role's `recursion_guard_env` set (applied by
20://! second echo (no echo-of-echo).
34:/// The role spawned for the echo-commune brief.
35:pub const ECHO_COMMUNE_ROLE: &str = "echo_commune";
37:/// Provenance tag stamped on echo-authored communes (drives the
39:pub const SOURCE_ECHO_COMMUNE: &str = "echo-commune";
41:/// Outcome of an echo run: the written drop-file path and the brief body.
48:/// An echo-commune failure. Carries an operator message; never panics, never
63:            EchoError::Runtime(e) => write!(f, "echo-commune spawn failed: {e}"),
65:                write!(f, "echo-commune exited {code:?}: {}", stderr.trim())
87:/// Run the echo-commune: feed `history` to the `[session.echo_commune]`
89:/// under `commune_dir`, tagged `Source: echo-commune`. spt-core is the **single
91:pub fn run_echo_commune(
129:    // stdin → stdout pass-through (the "summarizer" echoes the history it read).
138:    const GUARD_ECHO: &str = "cmd /C echo guard=%SPT_ECHO_COMMUNE%";
140:    const GUARD_ECHO: &str = "sh -c \"echo guard=$SPT_ECHO_COMMUNE\"";


87	/// Run the echo-commune: feed `history` to the `[session.echo_commune]`
88	/// summarizer (bounded), then write its brief as the `<id>-commune.md` drop-file
89	/// under `commune_dir`, tagged `Source: echo-commune`. spt-core is the **single
90	/// writer** of that file (6.4).
91	pub fn run_echo_commune(
92	    runtime: &dyn AgentRuntime,
93	    id: &str,
94	    keys: &BTreeMap<String, String>,
95	    history: &[HistoryRecord],
96	    commune_dir: &Path,
97	    timeout: Duration,
98	) -> Result<EchoResult, EchoError> {
99	    // Feed normalized history (newline-delimited) on stdin.
100	    let mut stdin = String::new();
101	    for r in history {
102	        stdin.push_str(&r.raw);
103	        stdin.push('\n');
104	    }
105	    let out = runtime.run_bounded_stdin(ECHO_COMMUNE_ROLE, keys, timeout, Some(stdin.as_bytes()))?;
106	    if !out.success() {
107	        return Err(EchoError::NonZero {
108	            code: out.status_code,
109	            stderr: out.stderr,
110	        });
111	    }
112
113	    let content = stamp_provenance(SOURCE_ECHO_COMMUNE, &out.stdout);
114	    std::fs::create_dir_all(commune_dir).map_err(EchoError::Write)?;
115	    let drop_path = commune_dir.join(format!("{id}-commune.md"));
116	    atomic_write_string(&drop_path, &content).map_err(EchoError::Write)?;
117
118	    Ok(EchoResult {
119	        drop_path,
120	        body: out.stdout,
121	    })
122	}
123
124	#[cfg(test)]
125	mod tests {
126	    use super::*;
127	    use spt_runtime::{Manifest, ManifestRuntime};
128
129	    // stdin → stdout pass-through (the "summarizer" echoes the history it read).
130	    // `sort` reads stdin with no file arg + needs no embedded quotes in the TOML
131	    // manifest (order-independent: the assertions check membership, not order).
132	    #[cfg(windows)]
133	    const PASSTHROUGH: &str = "sort";
134	    #[cfg(unix)]
135	    const PASSTHROUGH: &str = "cat";
136	    // Emits the recursion-guard env value (what an adapter hook keys off to bail).
137	    #[cfg(windows)]
138	    const GUARD_ECHO: &str = "cmd /C echo guard=%SPT_ECHO_COMMUNE%";
139	    #[cfg(unix)]
140	    const GUARD_ECHO: &str = "sh -c \"echo guard=$SPT_ECHO_COMMUNE\"";
141	    #[cfg(windows)]
142	    const FAIL: &str = "cmd /C exit 1";
143	    #[cfg(unix)]
144	    const FAIL: &str = "sh -c \"exit 1\"";
145
146	    fn echo_manifest(cmd: &str, guard: bool) -> Manifest {
147	        let guard_line = if guard {
148	            "recursion_guard_env=\"SPT_ECHO_COMMUNE\"\n"
149	        } else {
150	            ""
151	        };
152	        let toml = format!(
153	            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
154	             [session.echo_commune]\ncommand='{cmd}'\n{guard_line}"
155	        );
156	        Manifest::from_toml_str(&toml).unwrap()
157	    }
158
159	    fn recs(lines: &[&str]) -> Vec<HistoryRecord> {
160	        lines.iter().map(|l| HistoryRecord { raw: l.to_string() }).collect()
161	    }
162
163	    #[test] // [unit->REQ-SEAM-HISTORY] echo consumes history + emits a single-writer commune drop.
164	    fn echo_consumes_history_and_writes_commune() {
165	        let dir = tempfile::tempdir().unwrap();
166	        let rt = ManifestRuntime::new(echo_manifest(PASSTHROUGH, false));
167	        let history = recs(&["{\"r\":\"u\",\"t\":\"hi\"}", "{\"r\":\"a\",\"t\":\"yo\"}"]);
168	        let res = run_echo_commune(
169	            &rt,
170	            "doyle",
171	            &BTreeMap::new(),
172	            &history,
173	            dir.path(),
174	            Duration::from_secs(10),
175	        )
176	        .expect("echo");
177
178	        assert_eq!(res.drop_path, dir.path().join("doyle-commune.md"));
179	        let written = std::fs::read_to_string(&res.drop_path).unwrap();
180	        // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core wrote the drop file.
181	        assert!(written.starts_with("Source: echo-commune\n"), "provenance: {written:?}");
182	        assert!(written.contains("\"t\":\"hi\""), "consumed history: {written:?}");
183	        assert!(written.contains("\"t\":\"yo\""));
184	    }
185
186	    #[test] // [unit->REQ-SEAM-HISTORY] the echo child runs with the recursion guard set (won't re-echo).
187	    fn echo_child_is_recursion_guarded() {
188	        let dir = tempfile::tempdir().unwrap();
189	        let rt = ManifestRuntime::new(echo_manifest(GUARD_ECHO, true));
190	        let res = run_echo_commune(&rt, "doyle", &BTreeMap::new(), &[], dir.path(), Duration::from_secs(10))
191	            .expect("echo");
192	        assert!(res.body.contains("guard=1"), "guard env not set on echo child: {:?}", res.body);
193	    }
194
195	    #[test] // [unit->REQ-SEAM-HISTORY] a failing summarizer surfaces NonZero, no half-written drop.
196	    fn echo_nonzero_errors() {
197	        let dir = tempfile::tempdir().unwrap();
198	        let rt = ManifestRuntime::new(echo_manifest(FAIL, false));
199	        let e = run_echo_commune(&rt, "doyle", &BTreeMap::new(), &[], dir.path(), Duration::from_secs(10))
200	            .unwrap_err();
201	        assert!(matches!(e, EchoError::NonZero { .. }), "got {e}");
202	        assert!(!dir.path().join("doyle-commune.md").exists(), "no drop on failure");
203	    }
204
205	    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] provenance header is stamped by spt-core.
206	    fn provenance_is_stamped() {
207	        let s = stamp_provenance(SOURCE_ECHO_COMMUNE, "the brief body");
208	        assert_eq!(s, "Source: echo-commune\nthe brief body\n");
209	    }
210	}
211

/// Run the echo-commune: feed `history` to the `[session.echo_commune]`
/// summarizer (bounded), then write its brief as the `<id>-commune.md` drop-file
/// under `commune_dir`, tagged `Source: echo-commune`. spt-core is the **single
/// writer** of that file (6.4).
pub fn run_echo_commune(
    runtime: &dyn AgentRuntime,
    id: &str,
    keys: &BTreeMap<String, String>,
    history: &[HistoryRecord],
    commune_dir: &Path,
    timeout: Duration,
) -> Result<EchoResult, EchoError> {
    // Feed normalized history (newline-delimited) on stdin.
    let mut stdin = String::new();
    for r in history {
        stdin.push_str(&r.raw);
        stdin.push('\n');
    }
    let out = runtime.run_bounded_stdin(ECHO_COMMUNE_ROLE, keys, timeout, Some(stdin.as_bytes()))?;
    if !out.success() {
        return Err(EchoError::NonZero {
            code: out.status_code,
            stderr: out.stderr,
        });
    }

    let content = stamp_provenance(SOURCE_ECHO_COMMUNE, &out.stdout);
    std::fs::create_dir_all(commune_dir).map_err(EchoError::Write)?;
    let drop_path = commune_dir.join(format!("{id}-commune.md"));
    atomic_write_string(&drop_path, &content).map_err(EchoError::Write)?;

    Ok(EchoResult {
        drop_path,
        body: out.stdout,
    })
}

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

    // stdin → stdout pass-through (the "summarizer" echoes the history it read).
    // `sort` reads stdin with no file arg + needs no embedded quotes in the TOML
    // manifest (order-independent: the assertions check membership, not order).
    #[cfg(windows)]
    const PASSTHROUGH: &str = "sort";
    #[cfg(unix)]
    const PASSTHROUGH: &str = "cat";
    // Emits the recursion-guard env value (what an adapter hook keys off to bail).
    #[cfg(windows)]
    const GUARD_ECHO: &str = "cmd /C echo guard=%SPT_ECHO_COMMUNE%";
    #[cfg(unix)]
    const GUARD_ECHO: &str = "sh -c \"echo guard=$SPT_ECHO_COMMUNE\"";
    #[cfg(windows)]
    const FAIL: &str = "cmd /C exit 1";
    #[cfg(unix)]
    const FAIL: &str = "sh -c \"exit 1\"";

    fn echo_manifest(cmd: &str, guard: bool) -> Manifest {
        let guard_line = if guard {
            "recursion_guard_env=\"SPT_ECHO_COMMUNE\"\n"
        } else {
            ""
        };
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session.echo_commune]\ncommand='{cmd}'\n{guard_line}"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    fn recs(lines: &[&str]) -> Vec<HistoryRecord> {
        lines.iter().map(|l| HistoryRecord { raw: l.to_string() }).collect()
    }

    #[test] // [unit->REQ-SEAM-HISTORY] echo consumes history + emits a single-writer commune drop.
    fn echo_consumes_history_and_writes_commune() {
        let dir = tempfile::tempdir().unwrap();
        let rt = ManifestRuntime::new(echo_manifest(PASSTHROUGH, false));
        let history = recs(&["{\"r\":\"u\",\"t\":\"hi\"}", "{\"r\":\"a\",\"t\":\"yo\"}"]);
        let res = run_echo_commune(
            &rt,
            "doyle",
            &BTreeMap::new(),
            &history,
            dir.path(),
            Duration::from_secs(10),
        )
        .expect("echo");

        assert_eq!(res.drop_path, dir.path().join("doyle-commune.md"));
        let written = std::fs::read_to_string(&res.drop_path).unwrap();
        // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core wrote the drop file.
        assert!(written.starts_with("Source: echo-commune\n"), "provenance: {written:?}");
        assert!(written.contains("\"t\":\"hi\""), "consumed history: {written:?}");
        assert!(written.contains("\"t\":\"yo\""));
    }

    #[test] // [unit->REQ-SEAM-HISTORY] the echo child runs with the recursion guard set (won't re-echo).
    fn echo_child_is_recursion_guarded() {
        let dir = tempfile::tempdir().unwrap();
        let rt = ManifestRuntime::new(echo_manifest(GUARD_ECHO, true));
        let res = run_echo_commune(&rt, "doyle", &BTreeMap::new(), &[], dir.path(), Duration::from_secs(10))
            .expect("echo");
        assert!(res.body.contains("guard=1"), "guard env not set on echo child: {:?}", res.body);
    }

    #[test] // [unit->REQ-SEAM-HISTORY] a failing summarizer surfaces NonZero, no half-written drop.
    fn echo_nonzero_errors() {
        let dir = tempfile::tempdir().unwrap();
        let rt = ManifestRuntime::new(echo_manifest(FAIL, false));
        let e = run_echo_commune(&rt, "doyle", &BTreeMap::new(), &[], dir.path(), Duration::from_secs(10))
            .unwrap_err();
        assert!(matches!(e, EchoError::NonZero { .. }), "got {e}");
        assert!(!dir.path().join("doyle-commune.md").exists(), "no drop on failure");
    }

    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] provenance header is stamped by spt-core.
    fn provenance_is_stamped() {
        let s = stamp_provenance(SOURCE_ECHO_COMMUNE, "the brief body");
        assert_eq!(s, "Source: echo-commune\nthe brief body\n");
    }
}


Echo needs `[session.echo_commune]` in manifest. I'll build a richer test manifest. Now add the orphan-watch tests.


350	            }
351	            sleep_interruptible(self.cfg.pulse_period, stop);
352	        }
353	        None
354	    }
355	}
356
357	/// Sleep `period`, waking early if `stop` is set (10ms slices). Keeps the pulse
358	/// loop responsive to a kill without busy-spinning.
359	fn sleep_interruptible(period: Duration, stop: &AtomicBool) {
360	    const SLICE: Duration = Duration::from_millis(10);
361	    let mut left = period;
362	    while left > Duration::ZERO {
363	        if stop.load(Ordering::Relaxed) {
364	            return;
365	        }
366	        let nap = left.min(SLICE);
367	        std::thread::sleep(nap);
368	        left = left.saturating_sub(nap);
369	    }
370	}
371
372	#[cfg(test)]
373	mod tests {
374	    use super::*;
375	    use crate::test_home::with_home;
376	    use std::sync::atomic::AtomicUsize;
377
378	    // A live-capable manifest: a Psyche init template + a commune dir to watch.
379	    // The Psyche command is a trivial no-op (we never spawn it in these tests;
380	    // we exercise the pulse/ingest loop, which does not launch the Psyche).
381	    #[cfg(windows)]
382	    const NOOP_CMD: &str = "cmd /C rem";
383	    #[cfg(unix)]
384	    const NOOP_CMD: &str = "true";
385
386	    fn live_manifest(commune_dir: &std::path::Path) -> Manifest {
387	        let dir = commune_dir.to_string_lossy().replace('\\', "/");
388	        let toml = format!(
389	            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\

            }
            sleep_interruptible(self.cfg.pulse_period, stop);
        }
        None
    }
}

/// Sleep `period`, waking early if `stop` is set (10ms slices). Keeps the pulse
/// loop responsive to a kill without busy-spinning.
fn sleep_interruptible(period: Duration, stop: &AtomicBool) {
    const SLICE: Duration = Duration::from_millis(10);
    let mut left = period;
    while left > Duration::ZERO {
        if stop.load(Ordering::Relaxed) {
            return;
        }
        let nap = left.min(SLICE);
        std::thread::sleep(nap);
        left = left.saturating_sub(nap);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use std::sync::atomic::AtomicUsize;

    // A live-capable manifest: a Psyche init template + a commune dir to watch.
    // The Psyche command is a trivial no-op (we never spawn it in these tests;
    // we exercise the pulse/ingest loop, which does not launch the Psyche).
    #[cfg(windows)]
    const NOOP_CMD: &str = "cmd /C rem";
    #[cfg(unix)]
    const NOOP_CMD: &str = "true";

    fn live_manifest(commune_dir: &std::path::Path) -> Manifest {
        let dir = commune_dir.to_string_lossy().replace('\\', "/");
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\


386	    fn live_manifest(commune_dir: &std::path::Path) -> Manifest {
387	        let dir = commune_dir.to_string_lossy().replace('\\', "/");
388	        let toml = format!(
389	            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
390	             [session]\ncommune_dir=\"{dir}\"\n\n\
391	             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
392	        );
393	        Manifest::from_toml_str(&toml).unwrap()
394	    }
395
396	    #[test] // [unit->REQ-DAEMON-1] a daemon-hosted pulse tick ingests a commune drop.
397	    fn pulse_tick_ingests_drop() {
398	        with_home(|_| {
399	            let drops = tempfile::tempdir().unwrap();
400	            let manifest = live_manifest(drops.path());
401	            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
402	                .expect("live host");
403
404	            std::fs::write(drops.path().join("doyle-commune.md"), "brief").unwrap();
405	            let report = host.pulse_tick(Some("sid-1")).expect("tick");
406	            assert_eq!(report.ingested.len(), 1, "the drop was folded in");
407	            assert!(!report.echo_fired, "no echo gate armed");
408	            assert!(!drops.path().join("doyle-commune.md").exists(), "drop consumed");
409	        });
410	    }
411
412	    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted Psyche derives
413	    // liveness from the daemon `status`, not its summarizer pid: spawning marks
414	    // the perch `online`, so a dead summarizer subprocess does NOT flip it
415	    // offline; `mark_offline` is the only thing that does.
416	    #[test]
417	    fn daemon_hosted_psyche_liveness_is_status_not_pid() {
418	        use spt_store::info::{read_info, write_info};
419	        use spt_store::liveness::{is_perch_alive, STATUS_ONLINE};
420
421	        // A pid that is never a real allocation — stands in for the exited
422	        // summarizer subprocess (deterministic; no spawn-reap timing flake).
423	        const DEAD_PID: u32 = 2_000_000_000;
424
425	        with_home(|_| {
426	            let drops = tempfile::tempdir().unwrap();
427	            let manifest = live_manifest(drops.path());
428	            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
429	                .expect("live host");
430
431	            // Spawn the Psyche — the host binds the nested perch and marks it online.
432	            let handle = host.spawn_psyche(Some("sid-1"), Some("pulse")).expect("spawn psyche");
433	            assert_eq!(
434	                read_info(&handle.perch_path).expect("info.json").status.as_deref(),
435	                Some(STATUS_ONLINE),
436	                "the daemon marked the hosted Psyche online"
437	            );
438
439	            // Simulate the summarizer subprocess exiting: its recorded pid is now
440	            // dead, but the daemon `status` stays online.
441	            let mut rec = read_info(&handle.perch_path).unwrap();
442	            rec.pid = spt_store::info::PidValue::Numeric(DEAD_PID);
443	            write_info(&handle.perch_path, &rec).unwrap();
444	            assert!(
445	                is_perch_alive(&handle.perch_path),

    fn live_manifest(commune_dir: &std::path::Path) -> Manifest {
        let dir = commune_dir.to_string_lossy().replace('\\', "/");
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session]\ncommune_dir=\"{dir}\"\n\n\
             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    #[test] // [unit->REQ-DAEMON-1] a daemon-hosted pulse tick ingests a commune drop.
    fn pulse_tick_ingests_drop() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest(drops.path());
            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
                .expect("live host");

            std::fs::write(drops.path().join("doyle-commune.md"), "brief").unwrap();
            let report = host.pulse_tick(Some("sid-1")).expect("tick");
            assert_eq!(report.ingested.len(), 1, "the drop was folded in");
            assert!(!report.echo_fired, "no echo gate armed");
            assert!(!drops.path().join("doyle-commune.md").exists(), "drop consumed");
        });
    }

    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted Psyche derives
    // liveness from the daemon `status`, not its summarizer pid: spawning marks
    // the perch `online`, so a dead summarizer subprocess does NOT flip it
    // offline; `mark_offline` is the only thing that does.
    #[test]
    fn daemon_hosted_psyche_liveness_is_status_not_pid() {
        use spt_store::info::{read_info, write_info};
        use spt_store::liveness::{is_perch_alive, STATUS_ONLINE};

        // A pid that is never a real allocation — stands in for the exited
        // summarizer subprocess (deterministic; no spawn-reap timing flake).
        const DEAD_PID: u32 = 2_000_000_000;

        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest(drops.path());
            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
                .expect("live host");

            // Spawn the Psyche — the host binds the nested perch and marks it online.
            let handle = host.spawn_psyche(Some("sid-1"), Some("pulse")).expect("spawn psyche");
            assert_eq!(
                read_info(&handle.perch_path).expect("info.json").status.as_deref(),
                Some(STATUS_ONLINE),
                "the daemon marked the hosted Psyche online"
            );

            // Simulate the summarizer subprocess exiting: its recorded pid is now
            // dead, but the daemon `status` stays online.
            let mut rec = read_info(&handle.perch_path).unwrap();
            rec.pid = spt_store::info::PidValue::Numeric(DEAD_PID);
            write_info(&handle.perch_path, &rec).unwrap();
            assert!(
                is_perch_alive(&handle.perch_path),


Add echo-capable manifest helper after `live_manifest`.


Edit

             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    // A stdin→stdout passthrough so the echo-commune summarizer succeeds and
    // writes a real drop file — lets the signoff tests observe that the echo
    // (3.3) actually ran before any teardown.
    #[cfg(windows)]
    const PASSTHROUGH: &str = "sort";
    #[cfg(unix)]
    const PASSTHROUGH: &str = "cat";

    // A live manifest that ALSO declares an echo-commune summarizer, so
    // `fire_echo` (and thus the signoff echo step) succeeds and writes a commune.
    fn live_manifest_with_echo(commune_dir: &std::path::Path) -> Manifest {
        let dir = commune_dir.to_string_lossy().replace('\\', "/");
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session]\ncommune_dir=\"{dir}\"\n\n\
             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n\n\
             [session.echo_commune]\ncommand='{PASSTHROUGH}'\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    /// Create the perch dir + a `ready` marker for `id`, recording `parent_pid`
    /// as the supervised harness owner. Returns the perch path.
    fn seed_supervised_perch(id: &str, parent_pid: u32) -> std::path::PathBuf {
        use spt_store::info::{write_info, InfoJson};
        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let mut info = InfoJson::new(id, "epoch-ms:0", parent_pid, "sid-1", "ready");
        info.parent_pid = Some(parent_pid);
        write_info(&perch_path, &info).unwrap();
        let ready = perch::resolve_ready_file(id, ParentHint::Infer);
        if let Some(p) = ready.parent() {
            std::fs::create_dir_all(p).unwrap();
        }
        std::fs::write(&ready, "").unwrap();
        perch_path
    }

    // A pid that is never a real allocation (above Linux pid_max / Windows
    // ranges; positive as i32) — a deterministic stand-in for a dead owner.
    const DEAD_PID: u32 = 2_000_000_000;


             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    // A stdin→stdout passthrough so the echo-commune summarizer succeeds and
    // writes a real drop file — lets the signoff tests observe that the echo
    // (3.3) actually ran before any teardown.
    #[cfg(windows)]
    const PASSTHROUGH: &str = "sort";
    #[cfg(unix)]
    const PASSTHROUGH: &str = "cat";

    // A live manifest that ALSO declares an echo-commune summarizer, so
    // `fire_echo` (and thus the signoff echo step) succeeds and writes a commune.
    fn live_manifest_with_echo(commune_dir: &std::path::Path) -> Manifest {
        let dir = commune_dir.to_string_lossy().replace('\\', "/");
        let toml = format!(
            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\
             [session]\ncommune_dir=\"{dir}\"\n\n\
             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n\n\
             [session.echo_commune]\ncommand='{PASSTHROUGH}'\n"
        );
        Manifest::from_toml_str(&toml).unwrap()
    }

    /// Create the perch dir + a `ready` marker for `id`, recording `parent_pid`
    /// as the supervised harness owner. Returns the perch path.
    fn seed_supervised_perch(id: &str, parent_pid: u32) -> std::path::PathBuf {
        use spt_store::info::{write_info, InfoJson};
        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let mut info = InfoJson::new(id, "epoch-ms:0", parent_pid, "sid-1", "ready");
        info.parent_pid = Some(parent_pid);
        write_info(&perch_path, &info).unwrap();
        let ready = perch::resolve_ready_file(id, ParentHint::Infer);
        if let Some(p) = ready.parent() {
            std::fs::create_dir_all(p).unwrap();
        }
        std::fs::write(&ready, "").unwrap();
        perch_path
    }

    // A pid that is never a real allocation (above Linux pid_max / Windows
    // ranges; positive as i32) — a deterministic stand-in for a dead owner.
    const DEAD_PID: u32 = 2_000_000_000;

+    // A stdin→stdout passthrough so the echo-commune summarizer succeeds and

+    // writes a real drop file — lets the signoff tests observe that the echo

+    // (3.3) actually ran before any teardown.

+    // A live manifest that ALSO declares an echo-commune summarizer, so

+    // `fire_echo` (and thus the signoff echo step) succeeds and writes a commune.

+    fn live_manifest_with_echo(commune_dir: &std::path::Path) -> Manifest {

+        let dir = commune_dir.to_string_lossy().replace('\\', "/");

+            "[adapter]\nname=\"mock\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n\

+             [session]\ncommune_dir=\"{dir}\"\n\n\

+             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n\n\

+             [session.echo_commune]\ncommand='{PASSTHROUGH}'\n"

+        Manifest::from_toml_str(&toml).unwrap()

+    /// Create the perch dir + a `ready` marker for `id`, recording `parent_pid`

+    /// as the supervised harness owner. Returns the perch path.

+    fn seed_supervised_perch(id: &str, parent_pid: u32) -> std::path::PathBuf {

+        use spt_store::info::{write_info, InfoJson};

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

+        std::fs::create_dir_all(&perch_path).unwrap();

+        let mut info = InfoJson::new(id, "epoch-ms:0", parent_pid, "sid-1", "ready");

+        info.parent_pid = Some(parent_pid);

+        write_info(&perch_path, &info).unwrap();

+        let ready = perch::resolve_ready_file(id, ParentHint::Infer);

+        if let Some(p) = ready.parent() {

+            std::fs::create_dir_all(p).unwrap();

+        std::fs::write(&ready, "").unwrap();

+    // A pid that is never a real allocation (above Linux pid_max / Windows

+    // ranges; positive as i32) — a deterministic stand-in for a dead owner.

#[test] // [unit->REQ-DAEMON-1] a daemon-hosted pulse tick ingests a commune drop.


config: add crash_grace knob

Activate REQ-DAEMON-4 + tag evidence

Gate green + commit


Now append the orphan-watch tests at the end of the test module.


455	    // liveness from the daemon `status`, not its summarizer pid: spawning marks
456	    // the perch `online`, so a dead summarizer subprocess does NOT flip it
457	    // offline; `mark_offline` is the only thing that does.
458	    #[test]
459	    fn daemon_hosted_psyche_liveness_is_status_not_pid() {
460	        use spt_store::info::{read_info, write_info};
461	        use spt_store::liveness::{is_perch_alive, STATUS_ONLINE};
462
463	        // A pid that is never a real allocation — stands in for the exited
464	        // summarizer subprocess (deterministic; no spawn-reap timing flake).
465	        const DEAD_PID: u32 = 2_000_000_000;
466
467	        with_home(|_| {
468	            let drops = tempfile::tempdir().unwrap();
469	            let manifest = live_manifest(drops.path());
470	            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
471	                .expect("live host");
472
473	            // Spawn the Psyche — the host binds the nested perch and marks it online.
474	            let handle = host.spawn_psyche(Some("sid-1"), Some("pulse")).expect("spawn psyche");
475	            assert_eq!(
476	                read_info(&handle.perch_path).expect("info.json").status.as_deref(),
477	                Some(STATUS_ONLINE),
478	                "the daemon marked the hosted Psyche online"
479	            );
480
481	            // Simulate the summarizer subprocess exiting: its recorded pid is now
482	            // dead, but the daemon `status` stays online.
483	            let mut rec = read_info(&handle.perch_path).unwrap();
484	            rec.pid = spt_store::info::PidValue::Numeric(DEAD_PID);
485	            write_info(&handle.perch_path, &rec).unwrap();
486	            assert!(
487	                is_perch_alive(&handle.perch_path),
488	                "dead summarizer pid must NOT flip a daemon-hosted perch offline"
489	            );
490
491	            // Only an explicit teardown marks it offline.
492	            BrainLifecycle::mark_offline(&handle.perch_path);
493	            assert!(!is_perch_alive(&handle.perch_path), "mark_offline flips it offline");
494	        });

    // liveness from the daemon `status`, not its summarizer pid: spawning marks
    // the perch `online`, so a dead summarizer subprocess does NOT flip it
    // offline; `mark_offline` is the only thing that does.
    #[test]
    fn daemon_hosted_psyche_liveness_is_status_not_pid() {
        use spt_store::info::{read_info, write_info};
        use spt_store::liveness::{is_perch_alive, STATUS_ONLINE};

        // A pid that is never a real allocation — stands in for the exited
        // summarizer subprocess (deterministic; no spawn-reap timing flake).
        const DEAD_PID: u32 = 2_000_000_000;

        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest(drops.path());
            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
                .expect("live host");

            // Spawn the Psyche — the host binds the nested perch and marks it online.
            let handle = host.spawn_psyche(Some("sid-1"), Some("pulse")).expect("spawn psyche");
            assert_eq!(
                read_info(&handle.perch_path).expect("info.json").status.as_deref(),
                Some(STATUS_ONLINE),
                "the daemon marked the hosted Psyche online"
            );

            // Simulate the summarizer subprocess exiting: its recorded pid is now
            // dead, but the daemon `status` stays online.
            let mut rec = read_info(&handle.perch_path).unwrap();
            rec.pid = spt_store::info::PidValue::Numeric(DEAD_PID);
            write_info(&handle.perch_path, &rec).unwrap();
            assert!(
                is_perch_alive(&handle.perch_path),
                "dead summarizer pid must NOT flip a daemon-hosted perch offline"
            );

            // Only an explicit teardown marks it offline.
            BrainLifecycle::mark_offline(&handle.perch_path);
            assert!(!is_perch_alive(&handle.perch_path), "mark_offline flips it offline");
        });


496
497	    #[test] // [unit->REQ-DAEMON-1] the scheduled loop is config-paced + killable.
498	    fn pulse_loop_is_config_paced_and_killable() {
499	        with_home(|_| {
500	            let drops = tempfile::tempdir().unwrap();
501	            let manifest = live_manifest(drops.path());
502	            // Fast period so the test is quick; proves the period is config-driven.
503	            let cfg = DaemonConfig { pulse_period: Duration::from_millis(15), ..DaemonConfig::default() };
504	            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");
505	            assert_eq!(host.pulse_period(), Duration::from_millis(15));
506
507	            let stop = AtomicBool::new(false);
508	            let ticks = AtomicUsize::new(0);
509	            // Stop the loop from inside on_tick after a few ticks — proves the
510	            // loop actually schedules repeated ticks and honors the stop flag.
511	            host.run_pulse_loop(None, &stop, |_| {
512	                if ticks.fetch_add(1, Ordering::Relaxed) + 1 >= 3 {
513	                    stop.store(true, Ordering::Relaxed);
514	                }
515	            });
516	            assert!(ticks.load(Ordering::Relaxed) >= 3, "loop ran repeated ticks");
517	        });
518	    }
519	}
520

    #[test] // [unit->REQ-DAEMON-1] the scheduled loop is config-paced + killable.
    fn pulse_loop_is_config_paced_and_killable() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest(drops.path());
            // Fast period so the test is quick; proves the period is config-driven.
            let cfg = DaemonConfig { pulse_period: Duration::from_millis(15), ..DaemonConfig::default() };
            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");
            assert_eq!(host.pulse_period(), Duration::from_millis(15));

            let stop = AtomicBool::new(false);
            let ticks = AtomicUsize::new(0);
            // Stop the loop from inside on_tick after a few ticks — proves the
            // loop actually schedules repeated ticks and honors the stop flag.
            host.run_pulse_loop(None, &stop, |_| {
                if ticks.fetch_add(1, Ordering::Relaxed) + 1 >= 3 {
                    stop.store(true, Ordering::Relaxed);
                }
            });
            assert!(ticks.load(Ordering::Relaxed) >= 3, "loop ran repeated ticks");
        });
    }
}


            assert!(ticks.load(Ordering::Relaxed) >= 3, "loop ran repeated ticks");
        });
    }
}

            assert!(ticks.load(Ordering::Relaxed) >= 3, "loop ran repeated ticks");
        });
    }

    // [unit->REQ-DAEMON-4] the supervised-crash path runs the SAME ordering as a
    // graceful teardown: the echo-commune (3.3) fires and the perch is torn down
    // when the post-grace recheck confirms the harness owner is still gone.
    #[test]
    fn crash_signoff_tears_down_when_still_gone() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest_with_echo(drops.path());
            // A short crash_grace keeps the test quick (the grace wait is real).
            let cfg = DaemonConfig { crash_grace: Duration::from_millis(10), ..DaemonConfig::default() };
            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");

            let ready = perch::resolve_ready_file("doyle", ParentHint::Infer);
            std::fs::create_dir_all(ready.parent().unwrap()).unwrap();
            std::fs::write(&ready, "").unwrap();

            // still_gone = true: the Self did not recover across the grace window.
            let outcome = host.supervised_crash_signoff(Some("sid-1"), || true).expect("signoff");
            assert!(outcome.echo_ran, "echo-before-signoff (3.3) must have run");
            assert!(outcome.signed_off, "still-gone Self is torn down");
            assert!(
                drops.path().join("doyle-commune.md").exists(),
                "the echo wrote a final-delta commune before teardown"
            );
            assert!(!ready.exists(), "teardown removed the ready marker");
        });
    }

    // [unit->REQ-HAZARD-GRACE-BEFORE-SIGNOFF] a Self that recovers across the
    // grace window is NOT signed off — the phantom-INIT_SIGNOFF bug (1.1). The
    // echo still fires (it precedes the recheck), but no teardown happens.
    #[test]
    fn crash_signoff_spares_recovered_self() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest_with_echo(drops.path());
            let cfg = DaemonConfig { crash_grace: Duration::from_millis(10), ..DaemonConfig::default() };
            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");

            let ready = perch::resolve_ready_file("doyle", ParentHint::Infer);
            std::fs::create_dir_all(ready.parent().unwrap()).unwrap();
            std::fs::write(&ready, "").unwrap();

            // still_gone = false: the Self came back alive during the grace wait.
            let outcome = host.supervised_crash_signoff(Some("sid-1"), || false).expect("signoff");
            assert!(outcome.echo_ran, "echo precedes the recheck, so it still ran");
            assert!(!outcome.signed_off, "a recovered Self must never be signed off");
            assert!(ready.exists(), "the live Self's ready marker is left intact");
        });
    }

    // [unit->REQ-DAEMON-4] liveness for the supervised Self anchors on the stable
    // harness pid (`parent_pid`), never an ephemeral poll pid (2.1): a dead owner
    // reads gone, a live owner reads present, and no recorded owner reads gone.
    #[test]
    fn harness_owner_gone_anchors_on_parent_pid() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest(drops.path());
            let host = BrainLifecycle::with_config(&manifest, "doyle", DaemonConfig::default())
                .expect("live host");

            // No perch yet → no recorded owner → treated as gone.
            assert!(host.harness_owner_gone(), "an unsupervised perch reads gone");

            // A live owner (this very process) reads present.
            seed_supervised_perch("doyle", std::process::id());
            assert!(!host.harness_owner_gone(), "a live parent_pid reads present");

            // A dead owner reads gone.
            seed_supervised_perch("doyle", DEAD_PID);
            assert!(host.harness_owner_gone(), "a dead parent_pid reads gone");
        });
    }

    // [unit->REQ-DAEMON-4] the orphan-watch loop fires the crash signoff on an
    // observed owner death, sweeping a stale sentinel first (3.2) and tearing the
    // perch down once the recheck confirms the dead owner.
    #[test]
    fn orphan_watch_fires_on_dead_owner() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest_with_echo(drops.path());
            let cfg = DaemonConfig {
                pulse_period: Duration::from_millis(10),
                crash_grace: Duration::from_millis(10),
                ..DaemonConfig::default()
            };
            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");

            // Supervise a perch whose harness owner is already dead.
            seed_supervised_perch("doyle", DEAD_PID);
            let ready = perch::resolve_ready_file("doyle", ParentHint::Infer);
            assert!(ready.exists(), "precondition: ready marker present");

            let stop = AtomicBool::new(false);
            let outcome = host
                .run_orphan_watch(Some("sid-1"), DEAD_PID, &stop)
                .expect("watch fired")
                .expect("signoff ok");
            assert!(outcome.echo_ran && outcome.signed_off, "dead owner → echo + teardown");
            assert!(!ready.exists(), "the orphan-watch tore the perch down");
        });
    }

    // [unit->REQ-DAEMON-4] the orphan-watch honors the stop flag and never fires
    // while the supervised owner is alive (no false-positive teardown).
    #[test]
    fn orphan_watch_honors_stop_for_live_owner() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let manifest = live_manifest_with_echo(drops.path());
            let cfg = DaemonConfig { pulse_period: Duration::from_millis(10), ..DaemonConfig::default() };
            let host = BrainLifecycle::with_config(&manifest, "doyle", cfg).expect("live host");

            seed_supervised_perch("doyle", std::process::id());
            let ready = perch::resolve_ready_file("doyle", ParentHint::Infer);

            // Owner is this (alive) process; stop is already set → loop exits with
            // no signoff and leaves the perch untouched.
            let stop = AtomicBool::new(true);
            let outcome = host.run_orphan_watch(Some("sid-1"), std::process::id(), &stop);
            assert!(outcome.is_none(), "a live owner under stop never signs off");
            assert!(ready.exists(), "a live Self's perch is left intact");
        });
    }
}