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

pub fn write_context(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<WriteOutcome> {
    write_context_joined(path, body, source, now_ms, window_ms, None)
}

/// [`write_context`] with an extra **vector base** joined into the stamp — the
/// reconciled-merge write shape (ADR-0013): pass `join` = the union of the
/// conflicting versions' vectors so the accepted write carries
/// `file ⊔ join + bump(self)` and **strictly dominates every parent**,
/// clearing the conflict subnet-wide as it propagates.
pub fn write_context_joined(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
    join: Option<&spt_store::contextmark::VersionVector>,
) -> std::io::Result<WriteOutcome> {
    let existing = read_marker(path);
    let outcome = decide(existing.as_ref(), source, now_ms, window_ms);
    match outcome {
        WriteOutcome::Written { routed_at_ms } => {
            // Carry the file's causal history (joined with any merge parents);
            // bump our entry on an identified node. Pre-identity (no node.key)
            // stamps the v1 shape — never mint an identity from a context write.
            let mut vector = existing.map(|m| m.vector).unwrap_or_default();
            if let Some(base) = join {
                vector = vector.join(base);
            }
            let node = spt_store::nodeid::load_existing().map(|i| i.public_key().to_hex());
            if let Some(n) = &node {
                let mut epochs = EpochSource::load();
                vector.bump(n, epochs.next_epoch()?);
            }
            let marker = Marker { source, routed_at_ms, node, vector };
            let content = format!("{}\n{}", marker.render(), body);
            atomic_write_string(path, &content)?;
        }
        WriteOutcome::Suppressed { direct_at_ms } => {
            // [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] stale llm overwrite
            // within the window: suppress + log, never clobber the fresher write.
            eprintln!(
                "PRECEDENCE_SUPPRESS:{} (llm write within {}ms of direct write @ {})",
                path.display(),
                window_ms,
                direct_at_ms
            );
        }
    }
    Ok(outcome)
}

//! Precedence-guarded context store (M2b T4, KNOWN-HAZARDS 6.5; distributed
//! marker since M4-D6b).
//!
//! Every context write carries a **precedence marker** as the file's first
//! line — parse/render and the cross-node merge math live in
//! [`spt_store::contextmark`] (the sync path shares them); this module owns the
//! **local guard + the guarded file write**:
//!
//! ```text
//! <!-- spt:source=llm spt:routed_at_ms=… spt:node=… spt:vector=… -->
//! ```
//!
//! The local guard: a **direct** write always proceeds; an **llm** write
//! landing within the *protection window* after a recent **direct** write is
//! suppressed (and logged), so a stale LLM snapshot never clobbers a fresher
//! direct write.
//!
//! The distributed stamp (D6b): every accepted write carries the authoring
//! **node** and a **version vector** — the file's prior vector with this
//! node's entry bumped to a fresh [`EpochSource`](spt_store::epoch::EpochSource)
//! epoch (the same monotonic counter behind the D3b registry lease; wall-clock
//! never orders a merge). Pre-identity homes (no `node.key` yet) stamp the v1
//! marker shape — sync requires identity anyway, and a context write must
//! never mint one as a side effect.
//
// [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE]

use spt_store::atomic::atomic_write_string;
use spt_store::epoch::EpochSource;
use std::path::Path;

pub use spt_store::contextmark::{Marker, WriteSource};

/// Default protection window: an llm write within this long after a direct write
/// is suppressed. Interim default; per-endpoint override is a later knob.
pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 60_000;

/// Result of a guarded write.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WriteOutcome {
    /// The write landed; the new marker timestamp.
    Written { routed_at_ms: u64 },
    /// An llm write was suppressed within the protection window of a direct
    /// write; the file is unchanged.
    Suppressed { direct_at_ms: u64 },
}

/// Decide whether a write proceeds, given the existing marker (if any). Pure —
/// the testable core of the 6.5 guard. A direct write always wins; an llm write
/// is suppressed only when the *existing* marker is a direct write still inside
/// the protection window.
pub fn decide(
    existing: Option<&Marker>,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> WriteOutcome {
    if source == WriteSource::Direct {
        return WriteOutcome::Written { routed_at_ms: now_ms };
    }
    if let Some(m) = existing {
        if m.source == WriteSource::Direct && now_ms.saturating_sub(m.routed_at_ms) < window_ms {
            return WriteOutcome::Suppressed {
                direct_at_ms: m.routed_at_ms,
            };
        }
    }
    WriteOutcome::Written { routed_at_ms: now_ms }
}

/// Read the leading precedence marker of a context file, if present.
pub fn read_marker(path: &Path) -> Option<Marker> {
    let s = std::fs::read_to_string(path).ok()?;
    Marker::parse_content(&s)
}

/// Apply a precedence-guarded write to a context file. On `Written`, the file
/// is atomically replaced with `marker-line + body` — the marker carrying this
/// node's identity and the file's vector advanced by a fresh epoch (the D6b
/// distributed stamp). On `Suppressed`, the file is left untouched and the
/// suppression is logged (the body is discarded as the stale snapshot it is).
pub fn write_context(
    path: &Path,
    body: &str,
    source: WriteSource,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<WriteOutcome> {
    let existing = read_marker(path);
    let outcome = decide(existing.as_ref(), source, now_ms, window_ms);
    match outcome {
        WriteOutcome::Written { routed_at_ms } => {
            // Carry the file's causal history; bump our entry on an identified
            // node. Pre-identity (no node.key) stamps the v1 shape — never mint
            // an identity from a context write.
            let mut vector = existing.map(|m| m.vector).unwrap_or_default();
            let node = spt_store::nodeid::load_existing().map(|i| i.public_key().to_hex());
            if let Some(n) = &node {
                let mut epochs = EpochSource::load();
                vector.bump(n, epochs.next_epoch()?);
            }
            let marker = Marker { source, routed_at_ms, node, vector };
            let content = format!("{}\n{}", marker.render(), body);
            atomic_write_string(path, &content)?;
        }
        WriteOutcome::Suppressed { direct_at_ms } => {
            // [impl->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] stale llm overwrite
            // within the window: suppress + log, never clobber the fresher write.
            eprintln!(
                "PRECEDENCE_SUPPRESS:{} (llm write within {}ms of direct write @ {})",
                path.display(),
                window_ms,
                direct_at_ms
            );
        }
    }
    Ok(outcome)
}

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

    fn m(source: WriteSource, routed_at_ms: u64) -> Marker {
        Marker {
            source,
            routed_at_ms,
            node: None,
            vector: Default::default(),
        }
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] a direct write always proceeds.
    fn direct_always_writes() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Direct, 1001, 60_000),
            WriteOutcome::Written { routed_at_ms: 1001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm within the window of a direct write is suppressed.
    fn llm_within_window_suppressed() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 1500, 60_000),
            WriteOutcome::Suppressed { direct_at_ms: 1000 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm past the window proceeds.
    fn llm_past_window_writes() {
        let prior = m(WriteSource::Direct, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 61_001, 60_000),
            WriteOutcome::Written { routed_at_ms: 61_001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] llm over a prior llm write proceeds (no direct to protect).
    fn llm_over_llm_writes() {
        let prior = m(WriteSource::Llm, 1000);
        assert_eq!(
            decide(Some(&prior), WriteSource::Llm, 1001, 60_000),
            WriteOutcome::Written { routed_at_ms: 1001 }
        );
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] guarded file write honors the decision.
    fn write_context_suppresses_stale_llm() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("context.md");
        write_context(&path, "DIRECT", WriteSource::Direct, 1000, 60_000).unwrap();
        let out = write_context(&path, "STALE", WriteSource::Llm, 1500, 60_000).unwrap();
        assert_eq!(out, WriteOutcome::Suppressed { direct_at_ms: 1000 });
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("DIRECT") && !content.contains("STALE"));
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] an identified node stamps node + a strictly-advancing vector.
    fn identified_write_stamps_advancing_vector() {
        crate::test_support::with_home(|_| {
            // Mint the node identity through the canonical creation path.
            let id = spt_store::nodeid::load_or_create().expect("mint node identity");
            let node_hex = id.public_key().to_hex();

            let dir = tempfile::tempdir().unwrap();
            let path = dir.path().join("context.md");
            write_context(&path, "v1", WriteSource::Llm, 1000, 60_000).unwrap();
            let m1 = read_marker(&path).expect("marker");
            assert_eq!(m1.node.as_deref(), Some(node_hex.as_str()));
            let e1 = *m1.vector.0.get(&node_hex).expect("own entry");

            write_context(&path, "v2", WriteSource::Llm, 2000, 60_000).unwrap();
            let m2 = read_marker(&path).expect("marker");
            let e2 = *m2.vector.0.get(&node_hex).expect("own entry");
            assert!(e2 > e1, "vector entry must strictly advance ({e1} -> {e2})");
            assert_eq!(
                m2.vector.compare(&m1.vector),
                spt_store::contextmark::VectorOrder::Dominates,
                "a successor local write dominates its predecessor"
            );
        });
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] pre-identity homes stamp the v1 marker shape (no minting).
    fn pre_identity_write_stamps_v1_shape() {
        crate::test_support::with_home(|home| {
            let dir = tempfile::tempdir().unwrap();
            let path = dir.path().join("context.md");
            write_context(&path, "body", WriteSource::Llm, 1000, 60_000).unwrap();
            let marker = read_marker(&path).expect("marker");
            assert_eq!(marker.node, None);
            assert!(marker.vector.0.is_empty());
            // No node.key was minted as a side effect.
            assert!(
                !home.join("identity").join("node.key").exists(),
                "context write must not mint an identity"
            );
        });
    }
}

+    write_context_joined(path, body, source, now_ms, window_ms, None)

+/// [`write_context`] with an extra **vector base** joined into the stamp — the

+/// reconciled-merge write shape (ADR-0013): pass `join` = the union of the

+/// conflicting versions' vectors so the accepted write carries

+/// `file ⊔ join + bump(self)` and **strictly dominates every parent**,

+/// clearing the conflict subnet-wide as it propagates.

+    join: Option<&spt_store::contextmark::VersionVector>,

-            // Carry the file's causal history; bump our entry on an identified

-            // node. Pre-identity (no node.key) stamps the v1 shape — never mint

-            // an identity from a context write.

+            // Carry the file's causal history (joined with any merge parents);

+            // bump our entry on an identified node. Pre-identity (no node.key)

+            // stamps the v1 shape — never mint an identity from a context write.

+                vector = vector.join(base);


Now reconcile.rs:


Write

//! Psyche conflict reconcile (ADR-0013, M4-D6b) — the mind merges its own mind.
//!
//! When cross-node sync surfaces a concurrent context pair (hazard 6.6:
//! durable `.conflicts/` artifacts beside the untouched local file), the
//! **active instance's node** runs one bounded, stdout-captured Psyche turn
//! ([`crate::turn`], the D7.5a driver) over both versions. The merged output
//! is written back with a vector of `local ⊔ (⊔ artifacts) + bump(self)` —
//! strictly dominating every parent — and only that **dominating write**
//! clears the artifacts.
//!
//! Failure is safe by construction (6.6): a turn error, empty/garbage output,
//! or a 6.5 suppression (a fresher direct write landed meanwhile) leaves the
//! artifacts exactly where they were, for retry at the next sync/activation.
//! Both versions are never lost. Election of *which* node reconciles is the
//! daemon's (registry-aware) call; this module is the per-file mechanism.
//
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]

use crate::context::{write_context_joined, WriteOutcome, WriteSource};
use crate::turn::{run_psyche_turn, TurnError};
use spt_runtime::AgentRuntime;
use spt_store::contextmark::{Marker, VersionVector};
use spt_store::contextstore::ContextStore;
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;

/// What a reconcile pass did for one file.
#[derive(Debug)]
pub enum ReconcileOutcome {
    /// No artifacts pending — nothing to do.
    NoConflict,
    /// Merged write landed; this many artifacts cleared.
    Reconciled { cleared: usize },
    /// The Psyche turn failed (error preserved); artifacts untouched.
    TurnFailed(TurnError),
    /// The merged write was 6.5-suppressed (fresher direct write); artifacts
    /// untouched — retry later re-merges against the new local version.
    WriteSuppressed,
}

/// Split a stored context blob into `(marker, body)` — marker line stripped
/// from the body fed to the Psyche (transport metadata, not mind content).
fn split_marker(content: &str) -> (Option<Marker>, &str) {
    match Marker::parse_content(content) {
        Some(m) => {
            let body = content.split_once('\n').map(|(_, rest)| rest).unwrap_or("");
            (Some(m), body)
        }
        None => (None, content),
    }
}

/// Compose the reconcile prompt: the instruction plus every version, with the
/// wall-clock hints (human tiebreakers only — the vectors already proved
/// concurrency; ADR-0013).
fn compose_prompt(file_rel: &str, local: &str, locals_ms: Option<u64>, others: &[(String, Option<u64>)]) -> String {
    let mut p = String::new();
    p.push_str(
        "RECONCILE_CONTEXT_CONFLICT: concurrent versions of one of your context \
         files follow. Merge them into a single coherent file that preserves \
         every fact and intention worth keeping from BOTH; prefer specifics over \
         summaries; drop exact duplicates. Output ONLY the merged file body — \
         no commentary, no code fences, no marker line.\n\n",
    );
    p.push_str(&format!("FILE: {file_rel}\n\n"));
    p.push_str(&format!(
        "=== LOCAL VERSION{} ===\n{}\n\n",
        locals_ms.map(|t| format!(" (written ~epoch-ms {t}, hint only)")).unwrap_or_default(),
        local.trim_end()
    ));
    for (i, (body, ms)) in others.iter().enumerate() {
        p.push_str(&format!(
            "=== REMOTE VERSION {}{} ===\n{}\n\n",
            i + 1,
            ms.map(|t| format!(" (written ~epoch-ms {t}, hint only)")).unwrap_or_default(),
            body.trim_end()
        ));
    }
    p
}

/// Validate + normalize the Psyche's merged output: non-empty after stripping
/// any echoed marker line / code fences. `None` = garbage (treat as turn
/// failure; artifacts stay).
fn sanitize_merged(raw: &str) -> Option<String> {
    let mut s = raw.trim();
    // Strip an echoed marker line (the prompt forbids it; tolerate anyway).
    if Marker::parse_content(s).is_some() {
        s = s.split_once('\n').map(|(_, rest)| rest).unwrap_or("").trim();
    }
    // Strip one optional wrapping code fence.
    if let Some(inner) = s.strip_prefix("```") {
        let inner = inner.split_once('\n').map(|(_, rest)| rest).unwrap_or(inner);
        s = inner.strip_suffix("```").unwrap_or(inner).trim();
    }
    if s.is_empty() {
        return None;
    }
    Some(s.to_string())
}

/// Reconcile one conflicted context file in `worktree` (the per-file
/// mechanism; the caller decided this node is the reconciler). Reads the
/// local version + every artifact, runs the bounded Psyche turn, writes the
/// dominating merged version, clears the artifacts — or leaves everything
/// untouched on any failure (6.6).
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
pub fn reconcile_file(
    runtime: &dyn AgentRuntime,
    cs: &ContextStore,
    worktree: &Path,
    file_rel: &str,
    keys: &BTreeMap<String, String>,
    timeout: Duration,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<ReconcileOutcome> {
    let artifacts = cs.list_conflicts(worktree, Some(file_rel))?;
    if artifacts.is_empty() {
        return Ok(ReconcileOutcome::NoConflict);
    }

    let file_path = worktree.join(file_rel);
    let local_raw = std::fs::read_to_string(&file_path).unwrap_or_default();
    let (local_marker, local_body) = split_marker(&local_raw);

    // Join every parent's vector — the merge base that makes the reconciled
    // write dominate all of them.
    let mut join = local_marker.as_ref().map(|m| m.vector.clone()).unwrap_or_default();
    let mut others: Vec<(String, Option<u64>)> = Vec::new();
    for a in &artifacts {
        let raw = std::fs::read_to_string(a)?;
        let (m, body) = split_marker(&raw);
        if let Some(m) = &m {
            join = join.join(&m.vector);
        }
        others.push((body.to_string(), m.as_ref().map(|m| m.routed_at_ms)));
    }

    let prompt = compose_prompt(
        file_rel,
        local_body,
        local_marker.as_ref().map(|m| m.routed_at_ms),
        &others,
    );

    let merged = match run_psyche_turn(runtime, keys, prompt.as_bytes(), timeout) {
        Ok(out) => match sanitize_merged(&out) {
            Some(m) => m,
            None => return Ok(ReconcileOutcome::TurnFailed(TurnError::EmptyOutput)),
        },
        Err(e) => {
            eprintln!("RECONCILE_TURN_FAILED:{file_rel}: {e} (artifacts preserved)");
            return Ok(ReconcileOutcome::TurnFailed(e));
        }
    };

    // The merged write: llm source (6.5 still guards a fresher direct write),
    // vector = file ⊔ join + bump(self) — dominates every parent.
    match write_context_joined(
        &file_path,
        &merged,
        WriteSource::Llm,
        now_ms,
        window_ms,
        Some(&join),
    )? {
        WriteOutcome::Written { .. } => {
            let cleared = cs.clear_conflicts(worktree, file_rel)?;
            // Checkpoint the merged file itself (clear_conflicts committed the
            // artifact removal; a second commit is avoided by committing once
            // here — commit_in_worktree is whole-tree).
            cs.branch_store().commit_in_worktree(
                worktree,
                &[],
                &format!("reconcile: {file_rel} (cleared {cleared})"),
            )?;
            eprintln!("RECONCILE_MERGED:{file_rel} (cleared {cleared} artifact(s))");
            Ok(ReconcileOutcome::Reconciled { cleared })
        }
        WriteOutcome::Suppressed { .. } => {
            // A fresher direct write owns the window; artifacts stay for retry.
            Ok(ReconcileOutcome::WriteSuppressed)
        }
    }
}

/// `true` iff this vector strictly dominates every artifact's vector — used by
/// sync apply to recognize an incoming write that resolves a surfaced conflict
/// (the reconciled merge arriving from the reconciler node).
pub fn dominates_all(incoming: &VersionVector, artifact_markers: &[Marker]) -> bool {
    use spt_store::contextmark::VectorOrder;
    artifact_markers
        .iter()
        .all(|m| incoming.compare(&m.vector) == VectorOrder::Dominates)
}

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

    // A "psyche" that emits a fixed merged body, ignoring stdin.
    #[cfg(windows)]
    const MERGER: &str = "cmd /C echo MERGED: kept-from-A kept-from-B";
    #[cfg(unix)]
    const MERGER: &str = "sh -c \"echo 'MERGED: kept-from-A kept-from-B'\"";
    // A turn that fails outright.
    #[cfg(windows)]
    const FAILER: &str = "cmd /C exit 3";
    #[cfg(unix)]
    const FAILER: &str = "sh -c \"exit 3\"";

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

    fn seed_conflict(cs: &ContextStore) -> std::path::PathBuf {
        let live = cs.live_context_path("doyle").unwrap();
        crate::context::write_context(&live, "LOCAL mind", WriteSource::Llm, 1000, 0).unwrap();
        let wt = live.parent().unwrap().to_path_buf();
        // Incoming concurrent version, marker included (as the sync path stores it).
        let incoming = "<!-- spt:source=llm spt:routed_at_ms=999 spt:node=bb spt:vector=bb:7 -->\nREMOTE mind";
        cs.record_conflict(&wt, "live-context.md", incoming.as_bytes()).unwrap();
        wt
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a successful reconcile writes a dominating merge + clears artifacts.
    fn reconcile_merges_and_clears() {
        with_home(|_| {
            // Identified node so the merged vector can dominate.
            spt_store::nodeid::load_or_create().unwrap();
            let cs = ContextStore::open_or_init().unwrap();
            let wt = seed_conflict(&cs);

            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                5000,
                0,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::Reconciled { cleared: 1 }), "got {out:?}");

            // Merged content landed; artifacts gone.
            let content = std::fs::read_to_string(wt.join("live-context.md")).unwrap();
            assert!(content.contains("MERGED:"), "{content:?}");
            assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());

            // The merged marker dominates BOTH parents (local vector ⊔ bb:7 + bump).
            let m = Marker::parse_content(&content).unwrap();
            assert!(m.vector.0.get("bb") == Some(&7), "carries the remote entry: {m:?}");
            assert!(m.vector.0.len() >= 2, "own bump present too");
        });
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a failed turn leaves BOTH versions exactly in place.
    fn failed_turn_preserves_everything() {
        with_home(|_| {
            spt_store::nodeid::load_or_create().unwrap();
            let cs = ContextStore::open_or_init().unwrap();
            let wt = seed_conflict(&cs);
            let before_local = std::fs::read_to_string(wt.join("live-context.md")).unwrap();

            let out = reconcile_file(
                &rt(FAILER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                5000,
                0,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::TurnFailed(_)), "got {out:?}");
            assert_eq!(
                std::fs::read_to_string(wt.join("live-context.md")).unwrap(),
                before_local,
                "local untouched"
            );
            assert_eq!(cs.list_conflicts(&wt, None).unwrap().len(), 1, "artifact preserved");
        });
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a 6.5-suppressed merge write preserves the artifacts for retry.
    fn suppressed_write_preserves_artifacts() {
        with_home(|_| {
            spt_store::nodeid::load_or_create().unwrap();
            let cs = ContextStore::open_or_init().unwrap();
            let wt = seed_conflict(&cs);
            // A fresh DIRECT write inside the protection window.
            crate::context::write_context(
                &wt.join("live-context.md"),
                "FRESH DIRECT",
                WriteSource::Direct,
                4000,
                60_000,
            )
            .unwrap();

            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                5000,
                60_000,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::WriteSuppressed), "got {out:?}");
            let content = std::fs::read_to_string(wt.join("live-context.md")).unwrap();
            assert!(content.contains("FRESH DIRECT"), "direct write stays supreme");
            assert_eq!(cs.list_conflicts(&wt, None).unwrap().len(), 1, "artifact preserved");
        });
    }

    #[test] // no artifacts → NoConflict, nothing touched.
    fn no_conflict_is_a_noop() {
        with_home(|_| {
            let cs = ContextStore::open_or_init().unwrap();
            let live = cs.live_context_path("doyle").unwrap();
            let wt = live.parent().unwrap().to_path_buf();
            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                1000,
                0,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::NoConflict));
        });
    }
}


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

//! Psyche conflict reconcile (ADR-0013, M4-D6b) — the mind merges its own mind.
//!
//! When cross-node sync surfaces a concurrent context pair (hazard 6.6:
//! durable `.conflicts/` artifacts beside the untouched local file), the
//! **active instance's node** runs one bounded, stdout-captured Psyche turn
//! ([`crate::turn`], the D7.5a driver) over both versions. The merged output
//! is written back with a vector of `local ⊔ (⊔ artifacts) + bump(self)` —
//! strictly dominating every parent — and only that **dominating write**
//! clears the artifacts.
//!
//! Failure is safe by construction (6.6): a turn error, empty/garbage output,
//! or a 6.5 suppression (a fresher direct write landed meanwhile) leaves the
//! artifacts exactly where they were, for retry at the next sync/activation.
//! Both versions are never lost. Election of *which* node reconciles is the
//! daemon's (registry-aware) call; this module is the per-file mechanism.
//
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]

use crate::context::{write_context_joined, WriteOutcome, WriteSource};
use crate::turn::{run_psyche_turn, TurnError};
use spt_runtime::AgentRuntime;
use spt_store::contextmark::{Marker, VersionVector};
use spt_store::contextstore::ContextStore;
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;

/// What a reconcile pass did for one file.
#[derive(Debug)]
pub enum ReconcileOutcome {
    /// No artifacts pending — nothing to do.
    NoConflict,
    /// Merged write landed; this many artifacts cleared.
    Reconciled { cleared: usize },
    /// The Psyche turn failed (error preserved); artifacts untouched.
    TurnFailed(TurnError),
    /// The merged write was 6.5-suppressed (fresher direct write); artifacts
    /// untouched — retry later re-merges against the new local version.
    WriteSuppressed,
}

/// Split a stored context blob into `(marker, body)` — marker line stripped
/// from the body fed to the Psyche (transport metadata, not mind content).
fn split_marker(content: &str) -> (Option<Marker>, &str) {
    match Marker::parse_content(content) {
        Some(m) => {
            let body = content.split_once('\n').map(|(_, rest)| rest).unwrap_or("");
            (Some(m), body)
        }
        None => (None, content),
    }
}

/// Compose the reconcile prompt: the instruction plus every version, with the
/// wall-clock hints (human tiebreakers only — the vectors already proved
/// concurrency; ADR-0013).
fn compose_prompt(file_rel: &str, local: &str, locals_ms: Option<u64>, others: &[(String, Option<u64>)]) -> String {
    let mut p = String::new();
    p.push_str(
        "RECONCILE_CONTEXT_CONFLICT: concurrent versions of one of your context \
         files follow. Merge them into a single coherent file that preserves \
         every fact and intention worth keeping from BOTH; prefer specifics over \
         summaries; drop exact duplicates. Output ONLY the merged file body — \
         no commentary, no code fences, no marker line.\n\n",
    );
    p.push_str(&format!("FILE: {file_rel}\n\n"));
    p.push_str(&format!(
        "=== LOCAL VERSION{} ===\n{}\n\n",
        locals_ms.map(|t| format!(" (written ~epoch-ms {t}, hint only)")).unwrap_or_default(),
        local.trim_end()
    ));
    for (i, (body, ms)) in others.iter().enumerate() {
        p.push_str(&format!(
            "=== REMOTE VERSION {}{} ===\n{}\n\n",
            i + 1,
            ms.map(|t| format!(" (written ~epoch-ms {t}, hint only)")).unwrap_or_default(),
            body.trim_end()
        ));
    }
    p
}

/// Validate + normalize the Psyche's merged output: non-empty after stripping
/// any echoed marker line / code fences. `None` = garbage (treat as turn
/// failure; artifacts stay).
fn sanitize_merged(raw: &str) -> Option<String> {
    let mut s = raw.trim();
    // Strip an echoed marker line (the prompt forbids it; tolerate anyway).
    if Marker::parse_content(s).is_some() {
        s = s.split_once('\n').map(|(_, rest)| rest).unwrap_or("").trim();
    }
    // Strip one optional wrapping code fence.
    if let Some(inner) = s.strip_prefix("```") {
        let inner = inner.split_once('\n').map(|(_, rest)| rest).unwrap_or(inner);
        s = inner.strip_suffix("```").unwrap_or(inner).trim();
    }
    if s.is_empty() {
        return None;
    }
    Some(s.to_string())
}

/// Reconcile one conflicted context file in `worktree` (the per-file
/// mechanism; the caller decided this node is the reconciler). Reads the
/// local version + every artifact, runs the bounded Psyche turn, writes the
/// dominating merged version, clears the artifacts — or leaves everything
/// untouched on any failure (6.6).
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
pub fn reconcile_file(
    runtime: &dyn AgentRuntime,
    cs: &ContextStore,
    worktree: &Path,
    file_rel: &str,
    keys: &BTreeMap<String, String>,
    timeout: Duration,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<ReconcileOutcome> {
    let artifacts = cs.list_conflicts(worktree, Some(file_rel))?;
    if artifacts.is_empty() {
        return Ok(ReconcileOutcome::NoConflict);
    }

    let file_path = worktree.join(file_rel);
    let local_raw = std::fs::read_to_string(&file_path).unwrap_or_default();
    let (local_marker, local_body) = split_marker(&local_raw);

    // Join every parent's vector — the merge base that makes the reconciled
    // write dominate all of them.
    let mut join = local_marker.as_ref().map(|m| m.vector.clone()).unwrap_or_default();
    let mut others: Vec<(String, Option<u64>)> = Vec::new();
    for a in &artifacts {
        let raw = std::fs::read_to_string(a)?;
        let (m, body) = split_marker(&raw);
        if let Some(m) = &m {
            join = join.join(&m.vector);
        }
        others.push((body.to_string(), m.as_ref().map(|m| m.routed_at_ms)));
    }

    let prompt = compose_prompt(
        file_rel,
        local_body,
        local_marker.as_ref().map(|m| m.routed_at_ms),
        &others,
    );

    let merged = match run_psyche_turn(runtime, keys, prompt.as_bytes(), timeout) {
        Ok(out) => match sanitize_merged(&out) {
            Some(m) => m,
            None => return Ok(ReconcileOutcome::TurnFailed(TurnError::EmptyOutput)),
        },
        Err(e) => {
            eprintln!("RECONCILE_TURN_FAILED:{file_rel}: {e} (artifacts preserved)");
            return Ok(ReconcileOutcome::TurnFailed(e));
        }
    };

    // The merged write: llm source (6.5 still guards a fresher direct write),
    // vector = file ⊔ join + bump(self) — dominates every parent.
    match write_context_joined(
        &file_path,
        &merged,
        WriteSource::Llm,
        now_ms,
        window_ms,
        Some(&join),
    )? {
        WriteOutcome::Written { .. } => {
            let cleared = cs.clear_conflicts(worktree, file_rel)?;
            // Checkpoint the merged file itself (clear_conflicts committed the
            // artifact removal; a second commit is avoided by committing once
            // here — commit_in_worktree is whole-tree).
            cs.branch_store().commit_in_worktree(
                worktree,
                &[],
                &format!("reconcile: {file_rel} (cleared {cleared})"),
            )?;
            eprintln!("RECONCILE_MERGED:{file_rel} (cleared {cleared} artifact(s))");
            Ok(ReconcileOutcome::Reconciled { cleared })
        }
        WriteOutcome::Suppressed { .. } => {
            // A fresher direct write owns the window; artifacts stay for retry.
            Ok(ReconcileOutcome::WriteSuppressed)
        }
    }
}

/// `true` iff this vector strictly dominates every artifact's vector — used by
/// sync apply to recognize an incoming write that resolves a surfaced conflict
/// (the reconciled merge arriving from the reconciler node).
pub fn dominates_all(incoming: &VersionVector, artifact_markers: &[Marker]) -> bool {
    use spt_store::contextmark::VectorOrder;
    artifact_markers
        .iter()
        .all(|m| incoming.compare(&m.vector) == VectorOrder::Dominates)
}

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

    // A "psyche" that emits a fixed merged body, ignoring stdin.
    #[cfg(windows)]
    const MERGER: &str = "cmd /C echo MERGED: kept-from-A kept-from-B";
    #[cfg(unix)]
    const MERGER: &str = "sh -c \"echo 'MERGED: kept-from-A kept-from-B'\"";
    // A turn that fails outright.
    #[cfg(windows)]
    const FAILER: &str = "cmd /C exit 3";
    #[cfg(unix)]
    const FAILER: &str = "sh -c \"exit 3\"";

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

    fn seed_conflict(cs: &ContextStore) -> std::path::PathBuf {
        let live = cs.live_context_path("doyle").unwrap();
        crate::context::write_context(&live, "LOCAL mind", WriteSource::Llm, 1000, 0).unwrap();
        let wt = live.parent().unwrap().to_path_buf();
        // Incoming concurrent version, marker included (as the sync path stores it).
        let incoming = "<!-- spt:source=llm spt:routed_at_ms=999 spt:node=bb spt:vector=bb:7 -->\nREMOTE mind";
        cs.record_conflict(&wt, "live-context.md", incoming.as_bytes()).unwrap();
        wt
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a successful reconcile writes a dominating merge + clears artifacts.
    fn reconcile_merges_and_clears() {
        with_home(|_| {
            // Identified node so the merged vector can dominate.
            spt_store::nodeid::load_or_create().unwrap();
            let cs = ContextStore::open_or_init().unwrap();
            let wt = seed_conflict(&cs);

            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                5000,
                0,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::Reconciled { cleared: 1 }), "got {out:?}");

            // Merged content landed; artifacts gone.
            let content = std::fs::read_to_string(wt.join("live-context.md")).unwrap();
            assert!(content.contains("MERGED:"), "{content:?}");
            assert!(cs.list_conflicts(&wt, None).unwrap().is_empty());

            // The merged marker dominates BOTH parents (local vector ⊔ bb:7 + bump).
            let m = Marker::parse_content(&content).unwrap();
            assert!(m.vector.0.get("bb") == Some(&7), "carries the remote entry: {m:?}");
            assert!(m.vector.0.len() >= 2, "own bump present too");
        });
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a failed turn leaves BOTH versions exactly in place.
    fn failed_turn_preserves_everything() {
        with_home(|_| {
            spt_store::nodeid::load_or_create().unwrap();
            let cs = ContextStore::open_or_init().unwrap();
            let wt = seed_conflict(&cs);
            let before_local = std::fs::read_to_string(wt.join("live-context.md")).unwrap();

            let out = reconcile_file(
                &rt(FAILER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                5000,
                0,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::TurnFailed(_)), "got {out:?}");
            assert_eq!(
                std::fs::read_to_string(wt.join("live-context.md")).unwrap(),
                before_local,
                "local untouched"
            );
            assert_eq!(cs.list_conflicts(&wt, None).unwrap().len(), 1, "artifact preserved");
        });
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a 6.5-suppressed merge write preserves the artifacts for retry.
    fn suppressed_write_preserves_artifacts() {
        with_home(|_| {
            spt_store::nodeid::load_or_create().unwrap();
            let cs = ContextStore::open_or_init().unwrap();
            let wt = seed_conflict(&cs);
            // A fresh DIRECT write inside the protection window.
            crate::context::write_context(
                &wt.join("live-context.md"),
                "FRESH DIRECT",
                WriteSource::Direct,
                4000,
                60_000,
            )
            .unwrap();

            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                5000,
                60_000,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::WriteSuppressed), "got {out:?}");
            let content = std::fs::read_to_string(wt.join("live-context.md")).unwrap();
            assert!(content.contains("FRESH DIRECT"), "direct write stays supreme");
            assert_eq!(cs.list_conflicts(&wt, None).unwrap().len(), 1, "artifact preserved");
        });
    }

    #[test] // no artifacts → NoConflict, nothing touched.
    fn no_conflict_is_a_noop() {
        with_home(|_| {
            let cs = ContextStore::open_or_init().unwrap();
            let live = cs.live_context_path("doyle").unwrap();
            let wt = live.parent().unwrap().to_path_buf();
            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                1000,
                0,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::NoConflict));
        });
    }
}


Edit

pub mod psyche;
pub mod pulse;
pub mod resume;
pub mod signoff;

pub mod psyche;
pub mod pulse;
pub mod reconcile;
pub mod resume;
pub mod signoff;
pub mod turn;


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)

pub mod psyche;
pub mod pulse;
pub mod reconcile;
pub mod resume;
pub mod signoff;
pub mod turn;

//! 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, route_slices, route_two_slice, DropKind, Ingested, Tier, TierWrite};
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 mod ingest;
pub mod psyche;
pub mod pulse;
+pub mod reconcile;
pub mod resume;
pub mod signoff;
+pub mod turn;
#[cfg(test)]
mod test_support;


pub use context::{write_context, WriteOutcome, WriteSource};

pub use context::{write_context, write_context_joined, WriteOutcome, WriteSource};
pub use reconcile::{reconcile_file, ReconcileOutcome};
pub use turn::{run_psyche_turn, TurnError, PSYCHE_TURN_ROLE};


pub use context::{write_context, write_context_joined, WriteOutcome, WriteSource};
pub use reconcile::{reconcile_file, ReconcileOutcome};
pub use turn::{run_psyche_turn, TurnError, PSYCHE_TURN_ROLE};

//! 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 reconcile;
pub mod resume;
pub mod signoff;
pub mod turn;

#[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, route_slices, route_two_slice, DropKind, Ingested, Tier, TierWrite};
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 context::{write_context, WriteOutcome, WriteSource};

+pub use context::{write_context, write_context_joined, WriteOutcome, WriteSource};

+pub use reconcile::{reconcile_file, ReconcileOutcome};

+pub use turn::{run_psyche_turn, TurnError, PSYCHE_TURN_ROLE};


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-live --lib --quiet 2>&1 | grep -E "test result|error\[|FAILED|panicked|---- " | head -20

Test spt-live turn + reconcile


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


Election predicate in daemon:


55	    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.
56	    Dormant,
57	    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active
58	    /// suppression can be reasoned about; the resolver skips it.
59	    Offline,
60	}
61
62	/// One endpoint as seen on one node: the node it runs on, its advertised [`Status`], and
63	/// the author node's monotonic [`epoch`](Self::epoch) (the merge-precedence key).
64	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65	pub struct Instance {
66	    /// The node this instance runs on (node-qualifier in `id@node` addressing).
67	    pub node: String,
68	    /// Advertised liveness.
69	    pub status: Status,
70	    /// The author node's per-node monotonic epoch at the time of this update. The sole
71	    /// precedence key in [`merge_instance`] — strictly-greater wins.
72	    pub epoch: u64,
73	}
74
75	/// The per-subnet registry: `endpoint_id → [instance...]`. `BTreeMap`/`Vec` for stable,
76	/// deterministic serde ordering (no hash-seed nondeterminism across nodes).
77	#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
78	pub struct SubnetRegistry {
79	    #[serde(default)]
80	    by_id: BTreeMap<String, Vec<Instance>>,
81	}
82
83	/// What [`merge_instance`] did with an update — useful for replication accounting and
84	/// for asserting the lease in tests.
85	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86	pub enum MergeOutcome {
87	    /// First instance for this `(endpoint_id, node)` — added.
88	    Inserted,
89	    /// A strictly-newer epoch superseded the stored instance.
90	    Updated,
91	    /// Equal or lower epoch — dropped as stale (the lease at work).
92	    Stale,
93	}
94

    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.
    Dormant,
    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active
    /// suppression can be reasoned about; the resolver skips it.
    Offline,
}

/// One endpoint as seen on one node: the node it runs on, its advertised [`Status`], and
/// the author node's monotonic [`epoch`](Self::epoch) (the merge-precedence key).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Instance {
    /// The node this instance runs on (node-qualifier in `id@node` addressing).
    pub node: String,
    /// Advertised liveness.
    pub status: Status,
    /// The author node's per-node monotonic epoch at the time of this update. The sole
    /// precedence key in [`merge_instance`] — strictly-greater wins.
    pub epoch: u64,
}

/// The per-subnet registry: `endpoint_id → [instance...]`. `BTreeMap`/`Vec` for stable,
/// deterministic serde ordering (no hash-seed nondeterminism across nodes).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubnetRegistry {
    #[serde(default)]
    by_id: BTreeMap<String, Vec<Instance>>,
}

/// What [`merge_instance`] did with an update — useful for replication accounting and
/// for asserting the lease in tests.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeOutcome {
    /// First instance for this `(endpoint_id, node)` — added.
    Inserted,
    /// A strictly-newer epoch superseded the stored instance.
    Updated,
    /// Equal or lower epoch — dropped as stale (the lease at work).
    Stale,
}


<task-notification>
<task-id>bpwsjtm3v</task-id>
<tool-use-id>toolu_01WknMzhWkd36jJKwgby652J</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bpwsjtm3v.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI for 7e9c2b5" completed (exit code 0)</summary>
</task-notification>


Write

//! Reconciler election (ADR-0013) — which node runs the Psyche reconcile turn
//! for a surfaced context conflict.
//!
//! Exactly **one** node may reconcile a given conflict, or two nodes mint
//! textually-different merges of the same pair and the pair re-conflicts (a
//! reconcile storm). The rule:
//!
//! 1. **The endpoint's Active instance's node** — the mind is being used
//!    there, the Psyche is warm there, and it matches the
//!    active-instance-authoritative single-writer rule. Multiple Active rows
//!    (a partition's transient) fall to the deterministic tiebreak below
//!    *among the Active rows*.
//! 2. **No Active instance anywhere** → the lexicographically lowest node id
//!    among non-Offline holders; no rows at all → self (a local-only endpoint
//!    reconciles locally).
//!
//! Worst case under partition both sides elect themselves, both merges
//! propagate, the merges conflict — **detected and re-reconciled, never
//! lost** (hazard 6.6); the election only bounds how often that happens.
//! The actual per-file merge mechanics live in [`spt_live::reconcile`].
//
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]

use spt_net::net::registry::{Instance, Status};

/// Decide whether `self_node` is the reconciler for an endpoint, given the
/// endpoint's registry rows (every instance, every node). Pure — the daemon
/// feeds it the live registry; tests feed it rows.
pub fn is_reconciler(rows: &[Instance], self_node: &str) -> bool {
    let active: Vec<&Instance> = rows.iter().filter(|i| i.status == Status::Active).collect();
    if !active.is_empty() {
        return active.iter().map(|i| i.node.as_str()).min() == Some(self_node);
    }
    let holders: Vec<&str> = rows
        .iter()
        .filter(|i| i.status != Status::Offline)
        .map(|i| i.node.as_str())
        .collect();
    match holders.iter().min() {
        Some(lowest) => *lowest == self_node,
        None => true, // no registry presence — local-only endpoint, reconcile here
    }
}

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

    fn inst(node: &str, status: Status) -> Instance {
        Instance { node: node.to_string(), status, epoch: 1 }
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] the Active instance's node reconciles; everyone else stands down.
    fn active_node_wins() {
        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Dormant)];
        assert!(is_reconciler(&rows, "bbb"));
        assert!(!is_reconciler(&rows, "aaa"), "dormant lower id must stand down");
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] no Active → lowest non-Offline node id; Offline rows never elected.
    fn fallback_lowest_non_offline() {
        let rows = vec![
            inst("ccc", Status::Dormant),
            inst("aaa", Status::Offline),
            inst("bbb", Status::Dormant),
        ];
        assert!(is_reconciler(&rows, "bbb"), "lowest NON-OFFLINE wins");
        assert!(!is_reconciler(&rows, "aaa"), "offline never reconciles");
        assert!(!is_reconciler(&rows, "ccc"));
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] two Actives (partition transient): deterministic tiebreak among them.
    fn double_active_tiebreaks_deterministically() {
        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Active)];
        assert!(is_reconciler(&rows, "aaa"));
        assert!(!is_reconciler(&rows, "bbb"));
    }

    #[test] // no rows at all: a local-only endpoint reconciles locally.
    fn no_rows_reconciles_locally() {
        assert!(is_reconciler(&[], "me"));
    }
}


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

//! Reconciler election (ADR-0013) — which node runs the Psyche reconcile turn
//! for a surfaced context conflict.
//!
//! Exactly **one** node may reconcile a given conflict, or two nodes mint
//! textually-different merges of the same pair and the pair re-conflicts (a
//! reconcile storm). The rule:
//!
//! 1. **The endpoint's Active instance's node** — the mind is being used
//!    there, the Psyche is warm there, and it matches the
//!    active-instance-authoritative single-writer rule. Multiple Active rows
//!    (a partition's transient) fall to the deterministic tiebreak below
//!    *among the Active rows*.
//! 2. **No Active instance anywhere** → the lexicographically lowest node id
//!    among non-Offline holders; no rows at all → self (a local-only endpoint
//!    reconciles locally).
//!
//! Worst case under partition both sides elect themselves, both merges
//! propagate, the merges conflict — **detected and re-reconciled, never
//! lost** (hazard 6.6); the election only bounds how often that happens.
//! The actual per-file merge mechanics live in [`spt_live::reconcile`].
//
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]

use spt_net::net::registry::{Instance, Status};

/// Decide whether `self_node` is the reconciler for an endpoint, given the
/// endpoint's registry rows (every instance, every node). Pure — the daemon
/// feeds it the live registry; tests feed it rows.
pub fn is_reconciler(rows: &[Instance], self_node: &str) -> bool {
    let active: Vec<&Instance> = rows.iter().filter(|i| i.status == Status::Active).collect();
    if !active.is_empty() {
        return active.iter().map(|i| i.node.as_str()).min() == Some(self_node);
    }
    let holders: Vec<&str> = rows
        .iter()
        .filter(|i| i.status != Status::Offline)
        .map(|i| i.node.as_str())
        .collect();
    match holders.iter().min() {
        Some(lowest) => *lowest == self_node,
        None => true, // no registry presence — local-only endpoint, reconcile here
    }
}

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

    fn inst(node: &str, status: Status) -> Instance {
        Instance { node: node.to_string(), status, epoch: 1 }
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] the Active instance's node reconciles; everyone else stands down.
    fn active_node_wins() {
        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Dormant)];
        assert!(is_reconciler(&rows, "bbb"));
        assert!(!is_reconciler(&rows, "aaa"), "dormant lower id must stand down");
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] no Active → lowest non-Offline node id; Offline rows never elected.
    fn fallback_lowest_non_offline() {
        let rows = vec![
            inst("ccc", Status::Dormant),
            inst("aaa", Status::Offline),
            inst("bbb", Status::Dormant),
        ];
        assert!(is_reconciler(&rows, "bbb"), "lowest NON-OFFLINE wins");
        assert!(!is_reconciler(&rows, "aaa"), "offline never reconciles");
        assert!(!is_reconciler(&rows, "ccc"));
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] two Actives (partition transient): deterministic tiebreak among them.
    fn double_active_tiebreaks_deterministically() {
        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Active)];
        assert!(is_reconciler(&rows, "aaa"));
        assert!(!is_reconciler(&rows, "bbb"));
    }

    #[test] // no rows at all: a local-only endpoint reconciles locally.
    fn no_rows_reconciles_locally() {
        assert!(is_reconciler(&[], "me"));
    }
}


Prompt:
<task-notification>
<task-id>bpwsjtm3v</task-id>
<tool-use-id>toolu_01WknMzhWkd36jJKwgby652J</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bpwsjtm3v.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI for 7e9c2b5" completed (exit code 0)</summary>
</task-notification>


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.


Bash

cd C:/Users/decid/Documents/projects/spt-core && tail -2 "C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-core/648159b6-16b2-4568-b643-7b0c98c1871d/tasks/bpwsjtm3v.output" && grep -n "pub mod\|mod reconcile" crates/spt-daemon/src/lib.rs | head -25

Check CI result + daemon lib modules


[{"conclusion":"","headSha":"7e9c2b54d68fd99210f3e5b133a64d254285ab76","status":"in_progress"}]
[{"conclusion":"success","headSha":"7e9c2b54d68fd99210f3e5b133a64d254285ab76","status":"completed"}]
106:pub mod access;
107:pub mod adapter_update;
108:pub mod attach;
109:pub mod brain;
110:pub mod broker;
111:pub mod codec;
112:pub mod config;
113:pub mod consent;
114:pub mod daemon;
115:pub mod digest;
116:pub mod digesthub;
117:pub mod effect;
118:pub mod endpoint;
119:pub mod frame;
120:pub mod lifecycle;
121:pub mod msg;
122:pub mod nethost;
123:pub mod relay;
124:pub mod release;
125:pub mod seedmap;
126:pub mod transport;
127:pub mod update;
128:pub mod wan;
129:pub mod xfer;


D6b-1 CI **green** (`7e9c2b5`). Wire daemon module:


Edit

pub mod nethost;
pub mod reconcile;
pub mod relay;


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

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


100	//! (R-ARCH-2).
101	//!
102	//! [`Envelope`]: frame::Envelope
103	//! [`Hello`]: frame::Hello
104	//! [`DaemonTransport`]: transport::DaemonTransport
105
106	pub mod access;
107	pub mod adapter_update;
108	pub mod attach;
109	pub mod brain;
110	pub mod broker;
111	pub mod codec;
112	pub mod config;
113	pub mod consent;
114	pub mod daemon;
115	pub mod digest;
116	pub mod digesthub;
117	pub mod effect;
118	pub mod endpoint;
119	pub mod frame;
120	pub mod lifecycle;
121	pub mod msg;
122	pub mod nethost;
123	pub mod relay;
124	pub mod release;
125	pub mod seedmap;
126	pub mod transport;
127	pub mod update;
128	pub mod wan;
129	pub mod xfer;
130
131	#[cfg(test)]
132	mod test_home;
133
134	pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
135	pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
136	pub use adapter_update::{
137	    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
138	    SkipReason,
139	};
140	pub use brain::{Brain, BrainState, BrokerEvent};
141	pub use broker::Broker;
142	pub use config::{DaemonConfig, RelayChoice};
143	pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
144	pub use daemon::{ensure_running, is_running, Daemon};
145	pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
146	pub use digesthub::{
147	    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
148	    update_to_json, DigestHub,
149	};

//! (R-ARCH-2).
//!
//! [`Envelope`]: frame::Envelope
//! [`Hello`]: frame::Hello
//! [`DaemonTransport`]: transport::DaemonTransport

pub mod access;
pub mod adapter_update;
pub mod attach;
pub mod brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod consent;
pub mod daemon;
pub mod digest;
pub mod digesthub;
pub mod effect;
pub mod endpoint;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod nethost;
pub mod relay;
pub mod release;
pub mod seedmap;
pub mod transport;
pub mod update;
pub mod wan;
pub mod xfer;

#[cfg(test)]
mod test_home;

pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
pub use adapter_update::{
    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
    SkipReason,
};
pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::{DaemonConfig, RelayChoice};
pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
pub use daemon::{ensure_running, is_running, Daemon};
pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
    update_to_json, DigestHub,
};


pub mod nethost;
pub mod reconcile;
pub mod relay;


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

pub mod nethost;
pub mod reconcile;
pub mod relay;

//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).
//!
//! ADR-0004 splits the logical daemon into two implementation layers so a
//! routine self-update swaps the logic without disturbing live endpoints:
//!
//! - a **broker** (stable kernel) holds *only* the un-transferable resources a
//!   live consumer would lose continuity on if the logic restarted — PTY
//!   masters (from [`spt_term`]), spawned harness children, accepted/listening
//!   sockets — and runs no logic;
//! - a **brain** (restartable logic) holds routing, registry, manifest parse,
//!   and the re-hosted [`spt_live`] lifecycle, attaching to the broker over a
//!   **versioned local IPC** so a *newer brain talks to an older broker*.
//!
//! **B0 (this commit)** lands the crate's layer position plus that versioned
//! IPC seam — the load-bearing contract everything else rides:
//!
//! - [`frame`] — the forward-compatible [`Envelope`] schema + the version
//!   [`Hello`] handshake (KNOWN-HAZARDS 2.3, [`REQ-HAZARD-HANDOFF-ARGV-COMPAT`]);
//! - [`codec`] — length-prefixed JSON framing over any [`std::io::Read`]/`Write`;
//! - [`transport`] — the OS-split carrier (named pipe / UDS) behind one
//!   [`DaemonTransport`] trait, mirroring `spt-term`'s `SessionSurface`.
//!
//! **B1 (this commit)** adds the **broker kernel** ([`broker`]) — the stable
//! half that hosts a real `spt-term` PTY child and relays its output/input over
//! the IPC, holding only the ADR-0004 §B resources and no logic — plus the
//! broker↔brain message catalog ([`msg`]).
//!
//! **B3 (this commit)** consolidates the interim no-daemon model into the brain:
//! the `api listen` pulse / Psyche / drop-ingest loops become scheduled
//! in-process loops ([`lifecycle::BrainLifecycle`]) paced by a durable,
//! configurable period ([`config::DaemonConfig`]) instead of the interim 5s
//! constant; the only residue left in the harness-hosted session is the thin,
//! stateless [`relay::Relay`]. The `spt` binary's `LiveHost` becomes a shim over
//! `BrainLifecycle` (re-host, not rewrite) ([`REQ-DAEMON-1`]).
//!
//! **B5 (this commit)** makes the broker↔brain boundary **exactly-once across a
//! brain crash** ([`effect::EffectJournal`]): an idempotent `input` carries a
//! durable `op_id`, and the broker dedups at the effect keyed by that id over a
//! broker-owned recovery anchor (KNOWN-HAZARDS 7.2, [`REQ-HAZARD-RESTART-IDEMPOTENT`];
//! Spike #6's two constraints — broker-owned anchor, dedup-at-effect-by-id —
//! instantiated on the real PTY-write boundary).
//!
//! **C0 (this commit)** opens M3c — the self-update engine ([`update`]): the
//! ADR-0004 §A update-class taxonomy ([`UpdateClass`] + [`classify`]) and the
//! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).
//! The brain-only apply *triggers* a handoff over the B2 substrate — snapshot →
//! drop → re-attach — so the live PTY child + its output stream survive the logic
//! swap untouched; it never re-implements the handoff and never touches a broker
//! resource.
//!
//! **C1 (this commit)** adds the verify-before-handoff gate ([`release`]): a
//! release is signature-verified ([`REQ-UPD-2`]) and hardened against rollback
//! ([`REQ-HAZARD-UPDATE-ROLLBACK`]) — Ed25519 over signed metadata, key
//! trust/revocation, channel pinning, metadata expiry, monotonic version, and a
//! SHA-256 binding the signature to the actual binary bytes. [`plan_verified`] is
//! the front door: it produces an [`UpdatePlan`] *only* for a release that passes
//! every check, so an unverified binary can never reach [`apply_brain_only`].
//!
//! **C3 (this commit)** conducts the **adapter ripple** ([`adapter_update`]):
//! after self-update, spt-core updates each registered adapter via its manifest
//! `[update]` avenue ([`REQ-UPD-5`], [`REQ-SEAM-UPDATE`]). `file_pull` payloads
//! are verified against the **adapter's own** key (manifest-declared) + SHA-256
//! (adapter content signing, ADR-0004 §D); `delegated` updaters are opaque, so
//! spt-core delegates the trust only when the adapter attests `self_verifies`,
//! else skips them as unverifiable.
//!
//! **D4b (this commit)** makes broker-held QUIC streams survive brain restarts
//! **gaplessly and exactly-once** (Spike #3 invariants A–D as real tests): each
//! stream's reads are buffered in a sequenced ring (the B2 `OutputLog`
//! discipline) a brain subscribes to with a resume cursor; sends/opens carry
//! durable `op_id`s journaled at the broker, so a restarted brain's re-drive
//! dedups instead of double-writing the wire.
//!
//! **D4a (this commit)** moves the node's network presence into the broker
//! ([`nethost::NetHost`], ADR-0004 §B): a dedicated tokio runtime hosts the
//! iroh endpoint + every accepted/dialed QUIC connection, so a brain restart
//! never interrupts a cross-node connection. The brain drives net operations
//! (endpoint-status, dial) over new IPC frames in the same forward-compatible
//! catalog; an `op_id`-carrying dial is exactly-once through the B5
//! [`effect::EffectJournal`]. The endpoint binds as the durable node identity
//! (`spt-store`'s `node.key`), with the relay stance from `daemon.json`.
//!
//! **D5a (this commit)** puts WAN message delivery on the D4 substrate: a
//! [`WanMessage`](spt_net::net::wanmsg::WanMessage) NDJSON feed over a
//! broker-owned QUIC stream funnels through [`wan::receive_wan`] — the ADR-0009
//! access-gate seam ([`access`], handshake-proven origin only, KNOWN-HAZARDS
//! 7.5) plus durable op-id dedup (7.2, wire-fed) — into the existing local
//! REQ-MSG delivery path.
//!
//! **D5b (this commit)** adds remote terminal attach ([`attach`], REQ-INST-8):
//! a byte-stream viewport onto a local session for an operator daemon on
//! another node — session output pumped as sequenced
//! [`AttachRecord`](spt_net::net::attach::AttachRecord)s (deduped at the
//! operator's render cursor), operator input through the B5 exactly-once PTY
//! path, the whole loop gated and restart-survivable purely from the D4/B5
//! machinery.
//!
//! The brain + gapless handoff (B2) and the rest of the M3b task table build on
//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK
//! (R-ARCH-2).
//!
//! [`Envelope`]: frame::Envelope
//! [`Hello`]: frame::Hello
//! [`DaemonTransport`]: transport::DaemonTransport

pub mod access;
pub mod adapter_update;
pub mod attach;
pub mod brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod consent;
pub mod daemon;
pub mod digest;
pub mod digesthub;
pub mod effect;
pub mod endpoint;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod nethost;
pub mod relay;
pub mod release;
pub mod seedmap;
pub mod transport;
pub mod update;
pub mod wan;
pub mod xfer;

#[cfg(test)]
mod test_home;

pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
pub use adapter_update::{
    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
    SkipReason,
};
pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::{DaemonConfig, RelayChoice};
pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
pub use daemon::{ensure_running, is_running, Daemon};
pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
    update_to_json, DigestHub,
};
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use endpoint::{broker_socket_name, daemon_pid_path, digest_socket_name, seed_socket_name};
pub use lifecycle::{BrainLifecycle, TickReport};
pub use nethost::{NetConfig, NetHost, NET_EFFECT_SESSION};
pub use relay::Relay;
pub use seedmap::{put_seed, take_seed, SeedRegistry};
pub use frame::{
    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
    MIN_COMPATIBLE_VERSION,
};
pub use transport::{DaemonTransport, LocalSocketTransport};
pub use release::{
    parse_verifying_key, sha256_hex, verify_artifact, verify_metadata, verify_signature,
    RejectReason, ReleaseMetadata, SignedRelease, VerifyPolicy,
};
pub use update::{
    apply_brain_only, classify, plan_update, plan_verified, ApplyError, BrokerAbi, ReleaseSpec,
    UpdateClass, UpdatePlan, BROKER_RESOURCE_ABI,
};
pub use xfer::{fetch_file, push_file, serve_xfer, XferServeOutcome};
pub use wan::{receive_wan, WanOutcome};

pub mod lifecycle;
pub mod msg;
pub mod nethost;
+pub mod reconcile;
pub mod relay;
pub mod release;
pub mod seedmap;


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-daemon --lib --quiet 2>&1 | grep -E "test result|error\[|FAILED" | head -8 && cargo clippy --workspace --all-targets 2>&1 | grep -cE "^warning: |^error"; echo DONE

Test daemon + clippy


test result: ok. 89 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.01s
3
DONE


cd C:/Users/decid/Documents/projects/spt-core && cargo clippy --workspace --all-targets 2>&1 | grep -E "^warning: |^error" -A10 | head -45

Show clippy findings


warning: this function has too many arguments (8/7)
   --> crates\spt-live\src\reconcile.rs:109:1
    |
109 | / pub fn reconcile_file(
110 | |     runtime: &dyn AgentRuntime,
111 | |     cs: &ContextStore,
112 | |     worktree: &Path,
...   |
117 | |     window_ms: u64,
118 | | ) -> std::io::Result<ReconcileOutcome> {
    | |______________________________________^
--
warning: `spt-live` (lib) generated 1 warning
warning: `spt-live` (lib test) generated 1 warning (1 duplicate)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s


Edit

/// Reconcile one conflicted context file in `worktree` (the per-file
/// mechanism; the caller decided this node is the reconciler). Reads the
/// local version + every artifact, runs the bounded Psyche turn, writes the
/// dominating merged version, clears the artifacts — or leaves everything
/// untouched on any failure (6.6).
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
pub fn reconcile_file(
    runtime: &dyn AgentRuntime,
    cs: &ContextStore,
    worktree: &Path,
    file_rel: &str,
    keys: &BTreeMap<String, String>,
    timeout: Duration,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<ReconcileOutcome> {
    let artifacts = cs.list_conflicts(worktree, Some(file_rel))?;

/// One reconcile request — the per-file inputs (callers build one per
/// conflicted file; the runtime + store ride alongside).
pub struct ReconcileRequest<'a> {
    /// The worktree holding the conflicted file + its artifacts.
    pub worktree: &'a Path,
    /// The file, worktree-relative (e.g. `live-context.md`).
    pub file_rel: &'a str,
    /// Substitution keys for the Psyche turn (typically `session_id`).
    pub keys: &'a BTreeMap<String, String>,
    /// Turn budget (5.3).
    pub timeout: Duration,
    /// Wall-clock stamp for the merged write's marker.
    pub now_ms: u64,
    /// The 6.5 protection window for the merged (llm) write.
    pub window_ms: u64,
}

/// Reconcile one conflicted context file (the per-file mechanism; the caller
/// decided this node is the reconciler — [`spt-daemon`]'s election). Reads the
/// local version + every artifact, runs the bounded Psyche turn, writes the
/// dominating merged version, clears the artifacts — or leaves everything
/// untouched on any failure (6.6).
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
pub fn reconcile_file(
    runtime: &dyn AgentRuntime,
    cs: &ContextStore,
    req: &ReconcileRequest<'_>,
) -> std::io::Result<ReconcileOutcome> {
    let ReconcileRequest { worktree, file_rel, keys, timeout, now_ms, window_ms } = *req;
    let artifacts = cs.list_conflicts(worktree, Some(file_rel))?;


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

/// Reconcile one conflicted context file in `worktree` (the per-file
/// mechanism; the caller decided this node is the reconciler). Reads the
/// local version + every artifact, runs the bounded Psyche turn, writes the
/// dominating merged version, clears the artifacts — or leaves everything
/// untouched on any failure (6.6).
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
pub fn reconcile_file(
    runtime: &dyn AgentRuntime,
    cs: &ContextStore,
    worktree: &Path,
    file_rel: &str,
    keys: &BTreeMap<String, String>,
    timeout: Duration,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<ReconcileOutcome> {
    let artifacts = cs.list_conflicts(worktree, Some(file_rel))?;

/// One reconcile request — the per-file inputs (callers build one per
/// conflicted file; the runtime + store ride alongside).
pub struct ReconcileRequest<'a> {
    /// The worktree holding the conflicted file + its artifacts.
    pub worktree: &'a Path,
    /// The file, worktree-relative (e.g. `live-context.md`).
    pub file_rel: &'a str,
    /// Substitution keys for the Psyche turn (typically `session_id`).
    pub keys: &'a BTreeMap<String, String>,
    /// Turn budget (5.3).
    pub timeout: Duration,
    /// Wall-clock stamp for the merged write's marker.
    pub now_ms: u64,
    /// The 6.5 protection window for the merged (llm) write.
    pub window_ms: u64,
}

/// Reconcile one conflicted context file (the per-file mechanism; the caller
/// decided this node is the reconciler — [`spt-daemon`]'s election). Reads the
/// local version + every artifact, runs the bounded Psyche turn, writes the
/// dominating merged version, clears the artifacts — or leaves everything
/// untouched on any failure (6.6).
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
pub fn reconcile_file(
    runtime: &dyn AgentRuntime,
    cs: &ContextStore,
    req: &ReconcileRequest<'_>,
) -> std::io::Result<ReconcileOutcome> {
    let ReconcileRequest { worktree, file_rel, keys, timeout, now_ms, window_ms } = *req;
    let artifacts = cs.list_conflicts(worktree, Some(file_rel))?;

-/// Reconcile one conflicted context file in `worktree` (the per-file

-/// mechanism; the caller decided this node is the reconciler). Reads the

+/// One reconcile request — the per-file inputs (callers build one per

+/// conflicted file; the runtime + store ride alongside).

+    /// The worktree holding the conflicted file + its artifacts.

+    /// The file, worktree-relative (e.g. `live-context.md`).

+    /// Substitution keys for the Psyche turn (typically `session_id`).

+    pub keys: &'a BTreeMap<String, String>,

+    /// Wall-clock stamp for the merged write's marker.

+    /// The 6.5 protection window for the merged (llm) write.

+/// Reconcile one conflicted context file (the per-file mechanism; the caller

+/// decided this node is the reconciler — [`spt-daemon`]'s election). Reads the

+    let ReconcileRequest { worktree, file_rel, keys, timeout, now_ms, window_ms } = *req;


Bash

cd C:/Users/decid/Documents/projects/spt-core && python - <<'EOF'
import re
p = r'crates/spt-live/src/reconcile.rs'
s = open(p, encoding='utf-8').read()
old_calls = re.findall(r'reconcile_file\(\n(?:.*\n)*?            \)', s)
# Replace the four test call sites: pattern (&rt(X), &cs, &wt, "live-context.md", &BTreeMap::new(), Duration::from_secs(30), NOW, WIN,)
def repl(m):
    inner = m.group(0)
    return inner
pat = re.compile(r'reconcile_file\(\s*&rt\((\w+)\),\s*&cs,\s*&wt,\s*"live-context\.md",\s*&BTreeMap::new\(\),\s*Duration::from_secs\(30\),\s*(\d+),\s*(\d+),\s*\)')
def fix(m):
    return ('reconcile_file(\n                &rt(%s),\n                &cs,\n                &ReconcileRequest {\n                    worktree: &wt,\n                    file_rel: "live-context.md",\n                    keys: &BTreeMap::new(),\n                    timeout: Duration::from_secs(30),\n                    now_ms: %s,\n                    window_ms: %s,\n                },\n            )' % (m.group(1), m.group(2), m.group(3)))
s2, n = pat.subn(fix, s)
print("replaced", n)
open(p, 'w', encoding='utf-8', newline='\n').write(s2)
EOF

Rewrite reconcile test call sites to ReconcileRequest


replaced 3


1	//! Psyche conflict reconcile (ADR-0013, M4-D6b) — the mind merges its own mind.
2	//!
3	//! When cross-node sync surfaces a concurrent context pair (hazard 6.6:
4	//! durable `.conflicts/` artifacts beside the untouched local file), the
5	//! **active instance's node** runs one bounded, stdout-captured Psyche turn
6	//! ([`crate::turn`], the D7.5a driver) over both versions. The merged output
7	//! is written back with a vector of `local ⊔ (⊔ artifacts) + bump(self)` —
8	//! strictly dominating every parent — and only that **dominating write**
9	//! clears the artifacts.
10	//!
11	//! Failure is safe by construction (6.6): a turn error, empty/garbage output,
12	//! or a 6.5 suppression (a fresher direct write landed meanwhile) leaves the
13	//! artifacts exactly where they were, for retry at the next sync/activation.
14	//! Both versions are never lost. Election of *which* node reconciles is the
15	//! daemon's (registry-aware) call; this module is the per-file mechanism.
16	//
17	// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
18
19	use crate::context::{write_context_joined, WriteOutcome, WriteSource};
20	use crate::turn::{run_psyche_turn, TurnError};
21	use spt_runtime::AgentRuntime;
22	use spt_store::contextmark::{Marker, VersionVector};
23	use spt_store::contextstore::ContextStore;
24	use std::collections::BTreeMap;
25	use std::path::Path;
26	use std::time::Duration;
27
28	/// What a reconcile pass did for one file.
29	#[derive(Debug)]
30	pub enum ReconcileOutcome {
31	    /// No artifacts pending — nothing to do.
32	    NoConflict,
33	    /// Merged write landed; this many artifacts cleared.
34	    Reconciled { cleared: usize },
35	    /// The Psyche turn failed (error preserved); artifacts untouched.
36	    TurnFailed(TurnError),
37	    /// The merged write was 6.5-suppressed (fresher direct write); artifacts
38	    /// untouched — retry later re-merges against the new local version.
39	    WriteSuppressed,
40	}
41
42	/// Split a stored context blob into `(marker, body)` — marker line stripped
43	/// from the body fed to the Psyche (transport metadata, not mind content).
44	fn split_marker(content: &str) -> (Option<Marker>, &str) {
45	    match Marker::parse_content(content) {
46	        Some(m) => {
47	            let body = content.split_once('\n').map(|(_, rest)| rest).unwrap_or("");
48	            (Some(m), body)
49	        }
50	        None => (None, content),
51	    }
52	}
53
54	/// Compose the reconcile prompt: the instruction plus every version, with the
55	/// wall-clock hints (human tiebreakers only — the vectors already proved
56	/// concurrency; ADR-0013).
57	fn compose_prompt(file_rel: &str, local: &str, locals_ms: Option<u64>, others: &[(String, Option<u64>)]) -> String {
58	    let mut p = String::new();
59	    p.push_str(
60	        "RECONCILE_CONTEXT_CONFLICT: concurrent versions of one of your context \
61	         files follow. Merge them into a single coherent file that preserves \
62	         every fact and intention worth keeping from BOTH; prefer specifics over \
63	         summaries; drop exact duplicates. Output ONLY the merged file body — \
64	         no commentary, no code fences, no marker line.\n\n",
65	    );
66	    p.push_str(&format!("FILE: {file_rel}\n\n"));
67	    p.push_str(&format!(
68	        "=== LOCAL VERSION{} ===\n{}\n\n",
69	        locals_ms.map(|t| format!(" (written ~epoch-ms {t}, hint only)")).unwrap_or_default(),
70	        local.trim_end()
71	    ));
72	    for (i, (body, ms)) in others.iter().enumerate() {
73	        p.push_str(&format!(
74	            "=== REMOTE VERSION {}{} ===\n{}\n\n",
75	            i + 1,
76	            ms.map(|t| format!(" (written ~epoch-ms {t}, hint only)")).unwrap_or_default(),
77	            body.trim_end()
78	        ));
79	    }
80	    p
81	}
82
83	/// Validate + normalize the Psyche's merged output: non-empty after stripping
84	/// any echoed marker line / code fences. `None` = garbage (treat as turn
85	/// failure; artifacts stay).
86	fn sanitize_merged(raw: &str) -> Option<String> {
87	    let mut s = raw.trim();
88	    // Strip an echoed marker line (the prompt forbids it; tolerate anyway).
89	    if Marker::parse_content(s).is_some() {
90	        s = s.split_once('\n').map(|(_, rest)| rest).unwrap_or("").trim();
91	    }
92	    // Strip one optional wrapping code fence.
93	    if let Some(inner) = s.strip_prefix("```") {
94	        let inner = inner.split_once('\n').map(|(_, rest)| rest).unwrap_or(inner);
95	        s = inner.strip_suffix("```").unwrap_or(inner).trim();
96	    }
97	    if s.is_empty() {
98	        return None;
99	    }
100	    Some(s.to_string())
101	}
102
103	/// One reconcile request — the per-file inputs (callers build one per
104	/// conflicted file; the runtime + store ride alongside).
105	pub struct ReconcileRequest<'a> {
106	    /// The worktree holding the conflicted file + its artifacts.
107	    pub worktree: &'a Path,
108	    /// The file, worktree-relative (e.g. `live-context.md`).
109	    pub file_rel: &'a str,
110	    /// Substitution keys for the Psyche turn (typically `session_id`).
111	    pub keys: &'a BTreeMap<String, String>,
112	    /// Turn budget (5.3).
113	    pub timeout: Duration,
114	    /// Wall-clock stamp for the merged write's marker.
115	    pub now_ms: u64,
116	    /// The 6.5 protection window for the merged (llm) write.
117	    pub window_ms: u64,
118	}
119
120	/// Reconcile one conflicted context file (the per-file mechanism; the caller
121	/// decided this node is the reconciler — [`spt-daemon`]'s election). Reads the
122	/// local version + every artifact, runs the bounded Psyche turn, writes the
123	/// dominating merged version, clears the artifacts — or leaves everything
124	/// untouched on any failure (6.6).
125	// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]
126	pub fn reconcile_file(
127	    runtime: &dyn AgentRuntime,
128	    cs: &ContextStore,
129	    req: &ReconcileRequest<'_>,
130	) -> std::io::Result<ReconcileOutcome> {
131	    let ReconcileRequest { worktree, file_rel, keys, timeout, now_ms, window_ms } = *req;
132	    let artifacts = cs.list_conflicts(worktree, Some(file_rel))?;
133	    if artifacts.is_empty() {
134	        return Ok(ReconcileOutcome::NoConflict);
135	    }
136
137	    let file_path = worktree.join(file_rel);
138	    let local_raw = std::fs::read_to_string(&file_path).unwrap_or_default();
139	    let (local_marker, local_body) = split_marker(&local_raw);
140
141	    // Join every parent's vector — the merge base that makes the reconciled
142	    // write dominate all of them.
143	    let mut join = local_marker.as_ref().map(|m| m.vector.clone()).unwrap_or_default();
144	    let mut others: Vec<(String, Option<u64>)> = Vec::new();
145	    for a in &artifacts {
146	        let raw = std::fs::read_to_string(a)?;
147	        let (m, body) = split_marker(&raw);
148	        if let Some(m) = &m {
149	            join = join.join(&m.vector);
150	        }
151	        others.push((body.to_string(), m.as_ref().map(|m| m.routed_at_ms)));
152	    }
153
154	    let prompt = compose_prompt(
155	        file_rel,
156	        local_body,
157	        local_marker.as_ref().map(|m| m.routed_at_ms),
158	        &others,
159	    );
160
161	    let merged = match run_psyche_turn(runtime, keys, prompt.as_bytes(), timeout) {
162	        Ok(out) => match sanitize_merged(&out) {
163	            Some(m) => m,
164	            None => return Ok(ReconcileOutcome::TurnFailed(TurnError::EmptyOutput)),
165	        },
166	        Err(e) => {
167	            eprintln!("RECONCILE_TURN_FAILED:{file_rel}: {e} (artifacts preserved)");
168	            return Ok(ReconcileOutcome::TurnFailed(e));
169	        }
170	    };
171
172	    // The merged write: llm source (6.5 still guards a fresher direct write),
173	    // vector = file ⊔ join + bump(self) — dominates every parent.
174	    match write_context_joined(
175	        &file_path,
176	        &merged,
177	        WriteSource::Llm,
178	        now_ms,
179	        window_ms,
180	        Some(&join),
181	    )? {
182	        WriteOutcome::Written { .. } => {
183	            let cleared = cs.clear_conflicts(worktree, file_rel)?;

... [186 lines truncated] ...


cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-live --lib --quiet 2>&1 | grep -E "test result|error\[|FAILED" | head -6 && cargo clippy --workspace --all-targets 2>&1 | grep -cE "^warning: |^error"; echo DONE

Test + clippy after refactor


error[E0061]: this function takes 3 arguments but 8 arguments were supplied
3
DONE


126:pub fn reconcile_file(
127-    runtime: &dyn AgentRuntime,
128-    cs: &ContextStore,
129-    req: &ReconcileRequest<'_>,
130-) -> std::io::Result<ReconcileOutcome> {
131-    let ReconcileRequest { worktree, file_rel, keys, timeout, now_ms, window_ms } = *req;
132-    let artifacts = cs.list_conflicts(worktree, Some(file_rel))?;
133-    if artifacts.is_empty() {
134-        return Ok(ReconcileOutcome::NoConflict);
135-    }
136-
137-    let file_path = worktree.join(file_rel);
138-    let local_raw = std::fs::read_to_string(&file_path).unwrap_or_default();
--
255:            let out = reconcile_file(
256-                &rt(MERGER),
257-                &cs,
258-                &ReconcileRequest {
259-                    worktree: &wt,
260-                    file_rel: "live-context.md",
261-                    keys: &BTreeMap::new(),
262-                    timeout: Duration::from_secs(30),
263-                    now_ms: 5000,
264-                    window_ms: 0,
265-                },
266-            )
267-            .unwrap();
--
290:            let out = reconcile_file(
291-                &rt(FAILER),
292-                &cs,
293-                &ReconcileRequest {
294-                    worktree: &wt,
295-                    file_rel: "live-context.md",
296-                    keys: &BTreeMap::new(),
297-                    timeout: Duration::from_secs(30),
298-                    now_ms: 5000,
299-                    window_ms: 0,
300-                },
301-            )
302-            .unwrap();
--
329:            let out = reconcile_file(
330-                &rt(MERGER),
331-                &cs,
332-                &wt,
333-                "live-context.md",
334-                &BTreeMap::new(),
335-                Duration::from_secs(30),
336-                5000,
337-                60_000,
338-            )
339-            .unwrap();
340-            assert!(matches!(out, ReconcileOutcome::WriteSuppressed), "got {out:?}");
341-            let content = std::fs::read_to_string(wt.join("live-context.md")).unwrap();
--
353:            let out = reconcile_file(
354-                &rt(MERGER),
355-                &cs,
356-                &ReconcileRequest {
357-                    worktree: &wt,
358-                    file_rel: "live-context.md",
359-                    keys: &BTreeMap::new(),
360-                    timeout: Duration::from_secs(30),
361-                    now_ms: 1000,
362-                    window_ms: 0,
363-                },
364-            )
365-            .unwrap();


Edit

            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                5000,
                60_000,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::WriteSuppressed), "got {out:?}");

            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &ReconcileRequest {
                    worktree: &wt,
                    file_rel: "live-context.md",
                    keys: &BTreeMap::new(),
                    timeout: Duration::from_secs(30),
                    now_ms: 5000,
                    window_ms: 60_000,
                },
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::WriteSuppressed), "got {out:?}");


            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &wt,
                "live-context.md",
                &BTreeMap::new(),
                Duration::from_secs(30),
                5000,
                60_000,
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::WriteSuppressed), "got {out:?}");

            let out = reconcile_file(
                &rt(MERGER),
                &cs,
                &ReconcileRequest {
                    worktree: &wt,
                    file_rel: "live-context.md",
                    keys: &BTreeMap::new(),
                    timeout: Duration::from_secs(30),
                    now_ms: 5000,
                    window_ms: 60_000,
                },
            )
            .unwrap();
            assert!(matches!(out, ReconcileOutcome::WriteSuppressed), "got {out:?}");

-                Duration::from_secs(30),

+                    file_rel: "live-context.md",

+                    keys: &BTreeMap::new(),

+                    timeout: Duration::from_secs(30),


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-live -p spt-daemon --quiet 2>&1 | grep -E "test result|error\[|FAILED" | head -8 && cargo clippy --workspace --all-targets 2>&1 | grep -cE "^warning: |^error"; echo DONE

Re-test + clippy


test result: ok. 89 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.15s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.18s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.77s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.19s
0
DONE


Edit

pub use reconcile::{reconcile_file, ReconcileOutcome};

pub use reconcile::{reconcile_file, ReconcileOutcome, ReconcileRequest};


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 reconcile;
pub mod resume;
pub mod signoff;
pub mod turn;

#[cfg(test)]
mod test_support;

pub use context::{write_context, write_context_joined, WriteOutcome, WriteSource};
pub use reconcile::{reconcile_file, ReconcileOutcome};
pub use turn::{run_psyche_turn, TurnError, PSYCHE_TURN_ROLE};
pub use echo::{run_echo_commune, stamp_provenance, EchoError, EchoResult};
pub use history::{fetch_history, HistoryError, HistoryRecord};
pub use ingest::{ingest_drops, route_slices, route_two_slice, DropKind, Ingested, Tier, TierWrite};
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 reconcile::{reconcile_file, ReconcileOutcome};

+pub use reconcile::{reconcile_file, ReconcileOutcome, ReconcileRequest};


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test --workspace --quiet 2>&1 | grep -E "FAILED|panicked|error\[" | head -5; traceable-reqs check > /dev/null 2>&1; echo "trace_exit=$?"; cargo test -p spt-live --lib --quiet 2>&1 | grep "test result"

Full workspace tests + traceability


trace_exit=0
test result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.93s


All green. Commit D6b-2: