    pub fn project_context_path(&self, project_id: &str, id: &str) -> std::io::Result<PathBuf> {
        Self::validated(project_id)?;
        Self::validated(id)?;
        let file = project_context_file(&self.root, project_id, id);
        let wt = self.root.join("projects").join(project_id);
        self.store.ensure_worktree(&project_branch(project_id), &wt)?;
        std::fs::create_dir_all(file.parent().expect("project-context file has an agent dir"))?;
        Ok(file)
    }

//! The two-tier context store — `tracked/` realized over [`BranchStore`]
//! (M4-D6a; STORAGE §tracked/, ADR-0011, ADR-0013).
//!
//! Layout (the synced *mind*, physically separate from node-local `perches/`):
//!
//! ```text
//! tracked/
//!   .seed.git                    bare BranchStore repo (the one true object DB)
//!   agents/<id>/                 linked worktree of branch  a-<id>
//!     live-context.md            per-agent live context  → syncs to ALL instances
//!   projects/<project_id>/       linked worktree of branch  p-<project_id>
//!     <id>/project-context.md    per-(agent,project)      → same-project instances
//! ```
//!
//! Branches are the sync/merge unit (`a-`/`p-` refs are what D6c's bundle
//! exchange ships); worktrees are the read/write view. Tier routing is the
//! CONTEXT-MEMORY two-tier split: the `a-` branch is the agent's mind
//! everywhere, the `p-` branch holds every agent's context for one project
//! (cross-agent synthesis + query-routing ride that shape).
//!
//! Writes here are **paths + commits only** — the precedence-marker guard
//! (6.5) stays in `spt-live::context`, which writes through the paths this
//! store hands out, then calls the commit hook. Single writer per branch
//! (active-instance-authoritative; 6.4).
// [impl->REQ-STORE-1]

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

use crate::branchstore::BranchStore;
use crate::perch::tracked_dir;
use spt_proto::id::validate_endpoint_id;

/// The per-agent live-context file inside an `a-<id>` worktree.
pub const LIVE_CONTEXT_FILE: &str = "live-context.md";
/// The per-(agent,project) context file inside a `p-<project>` worktree,
/// nested under the agent id.
pub const PROJECT_CONTEXT_FILE: &str = "project-context.md";

/// The branch carrying `id`'s live context (the per-agent view).
pub fn agent_branch(id: &str) -> String {
    format!("a-{id}")
}

/// The branch carrying `project_id`'s per-project context (all agents).
pub fn project_branch(project_id: &str) -> String {
    format!("p-{project_id}")
}

/// Pure path of `id`'s live-context file under a `tracked/` root — the
/// **read** form (no branch/worktree creation; readers must not mint store
/// state). The single layout authority both the ensure-paths and every reader
/// route through (6.1).
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn live_context_file(root: &Path, id: &str) -> PathBuf {
    root.join("agents").join(id).join(LIVE_CONTEXT_FILE)
}

/// Pure path of the per-(agent,project) context file under a `tracked/` root —
/// the **read** form (no creation).
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn project_context_file(root: &Path, project_id: &str, id: &str) -> PathBuf {
    root.join("projects").join(project_id).join(id).join(PROJECT_CONTEXT_FILE)
}

/// The two-tier context store rooted at a `tracked/` directory.
pub struct ContextStore {
    store: BranchStore,
    root: PathBuf,
}

impl ContextStore {
    /// Open (or initialize) the node's canonical store at
    /// [`tracked_dir`](crate::perch::tracked_dir).
    pub fn open_or_init() -> std::io::Result<ContextStore> {
        Self::open_or_init_in(&tracked_dir())
    }

    /// Open (or initialize) a store rooted at an explicit `tracked/` dir —
    /// the testable seam.
    // [impl->REQ-STORE-1]
    pub fn open_or_init_in(root: &Path) -> std::io::Result<ContextStore> {
        let store = BranchStore::open_or_init(&root.join(".seed.git"))?;
        Ok(ContextStore { store, root: root.to_path_buf() })
    }

    /// The underlying branch-as-KV store (D6c's bundle exchange works on it).
    pub fn branch_store(&self) -> &BranchStore {
        &self.store
    }

    /// The `tracked/` root this store lives under.
    pub fn root(&self) -> &Path {
        &self.root
    }

    fn validated(id: &str) -> std::io::Result<()> {
        validate_endpoint_id(id)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))
    }

    /// Ensure the `a-<id>` branch + worktree exist; return the live-context
    /// file path inside it (`tracked/agents/<id>/live-context.md`). The id is
    /// charset-validated (4.6) — branch names and worktree dirs are derived
    /// from it.
    // [impl->REQ-STORE-1]
    pub fn live_context_path(&self, id: &str) -> std::io::Result<PathBuf> {
        Self::validated(id)?;
        let file = live_context_file(&self.root, id);
        let wt = file.parent().expect("live-context file has a worktree parent");
        self.store.ensure_worktree(&agent_branch(id), wt)?;
        Ok(file)
    }

    /// Ensure the `p-<project_id>` branch + worktree exist; return the
    /// per-(agent,project) context file path inside it
    /// (`tracked/projects/<project_id>/<id>/project-context.md`). Both
    /// components are charset-validated (4.6; project ids come from
    /// [`crate::project::project_id_for_dir`], which emits the same charset).
    // [impl->REQ-STORE-1]
    pub fn project_context_path(&self, project_id: &str, id: &str) -> std::io::Result<PathBuf> {
        Self::validated(project_id)?;
        Self::validated(id)?;
        let wt = self.root.join("projects").join(project_id);
        self.store.ensure_worktree(&project_branch(project_id), &wt)?;
        let dir = wt.join(id);
        std::fs::create_dir_all(&dir)?;
        Ok(dir.join(PROJECT_CONTEXT_FILE))
    }

    /// Commit `id`'s live-context worktree — one checkpoint commit on `a-<id>`
    /// (no-op when nothing changed). Call after a guarded write through
    /// [`live_context_path`](Self::live_context_path).
    // [impl->REQ-STORE-1]
    pub fn commit_live(&self, id: &str, message: &str) -> std::io::Result<Option<String>> {
        Self::validated(id)?;
        let wt = self.root.join("agents").join(id);
        self.store.commit_in_worktree(&wt, &[], message)
    }

    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op
    /// when nothing changed).
    // [impl->REQ-STORE-1]
    pub fn commit_project(&self, project_id: &str, message: &str) -> std::io::Result<Option<String>> {
        Self::validated(project_id)?;
        let wt = self.root.join("projects").join(project_id);
        self.store.commit_in_worktree(&wt, &[], message)
    }
}

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

    #[test] // [unit->REQ-STORE-1] tier paths land on the documented layout; commits checkpoint per branch.
    fn two_tier_layout_and_commits() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();

        let live = cs.live_context_path("doyle").unwrap();
        assert!(live.ends_with(Path::new("agents").join("doyle").join(LIVE_CONTEXT_FILE)));
        std::fs::write(&live, "live mind").unwrap();
        let c = cs.commit_live("doyle", "live v1").unwrap();
        assert!(c.is_some());
        assert_eq!(
            cs.branch_store().read_at_tip(&agent_branch("doyle"), LIVE_CONTEXT_FILE).unwrap(),
            Some(b"live mind".to_vec())
        );

        let proj = cs.project_context_path("spt-core", "doyle").unwrap();
        assert!(proj.ends_with(
            Path::new("projects").join("spt-core").join("doyle").join(PROJECT_CONTEXT_FILE)
        ));
        std::fs::write(&proj, "project mind").unwrap();
        let c = cs.commit_project("spt-core", "proj v1").unwrap();
        assert!(c.is_some());
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&project_branch("spt-core"), "doyle/project-context.md")
                .unwrap(),
            Some(b"project mind".to_vec())
        );
    }

    #[test] // [unit->REQ-STORE-1] the p- branch aggregates multiple agents (cross-agent synthesis shape).
    fn project_branch_holds_many_agents() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        for id in ["doyle", "momo"] {
            let p = cs.project_context_path("spt-core", id).unwrap();
            std::fs::write(&p, format!("{id} ctx")).unwrap();
        }
        cs.commit_project("spt-core", "both").unwrap().unwrap();
        let bs = cs.branch_store();
        assert!(bs.read_at_tip(&project_branch("spt-core"), "doyle/project-context.md").unwrap().is_some());
        assert!(bs.read_at_tip(&project_branch("spt-core"), "momo/project-context.md").unwrap().is_some());
    }

    #[test] // [unit->REQ-HAZARD-ID-CHARSET] a path-smuggling id is refused before any branch/dir derives from it.
    fn invalid_id_refused() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        for bad in ["../up", "a/b", "a:b", "a@b", ""] {
            assert!(cs.live_context_path(bad).is_err(), "{bad:?} must refuse");
        }
    }
}

+        let file = project_context_file(&self.root, project_id, id);

-        std::fs::create_dir_all(&dir)?;

-        Ok(dir.join(PROJECT_CONTEXT_FILE))

+        std::fs::create_dir_all(file.parent().expect("project-context file has an agent dir"))?;


Now the rename arm on ContextStore:


Edit

    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op
    /// when nothing changed).
    // [impl->REQ-STORE-1]
    pub fn commit_project(&self, project_id: &str, message: &str) -> std::io::Result<Option<String>> {
        Self::validated(project_id)?;
        let wt = self.root.join("projects").join(project_id);
        self.store.commit_in_worktree(&wt, &[], message)
    }
}

    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op
    /// when nothing changed).
    // [impl->REQ-STORE-1]
    pub fn commit_project(&self, project_id: &str, message: &str) -> std::io::Result<Option<String>> {
        Self::validated(project_id)?;
        let wt = self.root.join("projects").join(project_id);
        self.store.commit_in_worktree(&wt, &[], message)
    }

    /// The context-store arm of `spt rename <old> <new>` (REQ-INST-11, the
    /// D3f-documented seam): move the `a-<old>` branch + its worktree to
    /// `a-<new>`, and rename `<old>/` → `<new>/` inside every project worktree
    /// (one rename commit per touched project). Refuses when `a-<new>` already
    /// exists (never merge two minds); an absent `a-<old>` is a no-op `Ok`
    /// (nothing tracked yet — perch-only endpoints rename cleanly).
    // [impl->REQ-INST-11]
    pub fn rename_endpoint(&self, old_id: &str, new_id: &str) -> std::io::Result<()> {
        Self::validated(old_id)?;
        Self::validated(new_id)?;
        if self.store.has_branch(&agent_branch(new_id))? {
            return Err(std::io::Error::new(
                std::io::ErrorKind::AlreadyExists,
                format!("context branch already exists: {}", agent_branch(new_id)),
            ));
        }

        // a- arm: move worktree dir, then rename the branch (git updates the
        // worktree's HEAD symref). Skip when the old branch never existed.
        if self.store.has_branch(&agent_branch(old_id))? {
            let old_wt = self.root.join("agents").join(old_id);
            let new_wt = self.root.join("agents").join(new_id);
            if old_wt.join(".git").exists() {
                crate::gitrun::run_git_ok(
                    &["worktree", "move", &old_wt.to_string_lossy(), &new_wt.to_string_lossy()],
                    Some(&old_wt.parent().expect("agents/ parent")),
                    None,
                )?;
            }
            crate::gitrun::run_git_ok(
                &["branch", "-m", &agent_branch(old_id), &agent_branch(new_id)],
                Some(&new_wt),
                None,
            )?;
        }

        // p- arm: rename the agent dir inside every project worktree holding one.
        let projects = self.root.join("projects");
        if let Ok(entries) = std::fs::read_dir(&projects) {
            for entry in entries.flatten() {
                let wt = entry.path();
                if !wt.join(".git").exists() || !wt.join(old_id).exists() {
                    continue;
                }
                crate::gitrun::run_git_ok(&["mv", old_id, new_id], Some(&wt), None)?;
                self.store.commit_in_worktree(
                    &wt,
                    &[],
                    &format!("rename {old_id} -> {new_id}"),
                )?;
            }
        }
        Ok(())
    }
}


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

    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op
    /// when nothing changed).
    // [impl->REQ-STORE-1]
    pub fn commit_project(&self, project_id: &str, message: &str) -> std::io::Result<Option<String>> {
        Self::validated(project_id)?;
        let wt = self.root.join("projects").join(project_id);
        self.store.commit_in_worktree(&wt, &[], message)
    }

    /// The context-store arm of `spt rename <old> <new>` (REQ-INST-11, the
    /// D3f-documented seam): move the `a-<old>` branch + its worktree to
    /// `a-<new>`, and rename `<old>/` → `<new>/` inside every project worktree
    /// (one rename commit per touched project). Refuses when `a-<new>` already
    /// exists (never merge two minds); an absent `a-<old>` is a no-op `Ok`
    /// (nothing tracked yet — perch-only endpoints rename cleanly).
    // [impl->REQ-INST-11]
    pub fn rename_endpoint(&self, old_id: &str, new_id: &str) -> std::io::Result<()> {
        Self::validated(old_id)?;
        Self::validated(new_id)?;
        if self.store.has_branch(&agent_branch(new_id))? {
            return Err(std::io::Error::new(
                std::io::ErrorKind::AlreadyExists,
                format!("context branch already exists: {}", agent_branch(new_id)),
            ));
        }

        // a- arm: move worktree dir, then rename the branch (git updates the
        // worktree's HEAD symref). Skip when the old branch never existed.
        if self.store.has_branch(&agent_branch(old_id))? {
            let old_wt = self.root.join("agents").join(old_id);
            let new_wt = self.root.join("agents").join(new_id);
            if old_wt.join(".git").exists() {
                crate::gitrun::run_git_ok(
                    &["worktree", "move", &old_wt.to_string_lossy(), &new_wt.to_string_lossy()],
                    Some(&old_wt.parent().expect("agents/ parent")),
                    None,
                )?;
            }
            crate::gitrun::run_git_ok(
                &["branch", "-m", &agent_branch(old_id), &agent_branch(new_id)],
                Some(&new_wt),
                None,
            )?;
        }

        // p- arm: rename the agent dir inside every project worktree holding one.
        let projects = self.root.join("projects");
        if let Ok(entries) = std::fs::read_dir(&projects) {
            for entry in entries.flatten() {
                let wt = entry.path();
                if !wt.join(".git").exists() || !wt.join(old_id).exists() {
                    continue;
                }
                crate::gitrun::run_git_ok(&["mv", old_id, new_id], Some(&wt), None)?;
                self.store.commit_in_worktree(
                    &wt,
                    &[],
                    &format!("rename {old_id} -> {new_id}"),
                )?;
            }
        }
        Ok(())
    }
}

//! The two-tier context store — `tracked/` realized over [`BranchStore`]
//! (M4-D6a; STORAGE §tracked/, ADR-0011, ADR-0013).
//!
//! Layout (the synced *mind*, physically separate from node-local `perches/`):
//!
//! ```text
//! tracked/
//!   .seed.git                    bare BranchStore repo (the one true object DB)
//!   agents/<id>/                 linked worktree of branch  a-<id>
//!     live-context.md            per-agent live context  → syncs to ALL instances
//!   projects/<project_id>/       linked worktree of branch  p-<project_id>
//!     <id>/project-context.md    per-(agent,project)      → same-project instances
//! ```
//!
//! Branches are the sync/merge unit (`a-`/`p-` refs are what D6c's bundle
//! exchange ships); worktrees are the read/write view. Tier routing is the
//! CONTEXT-MEMORY two-tier split: the `a-` branch is the agent's mind
//! everywhere, the `p-` branch holds every agent's context for one project
//! (cross-agent synthesis + query-routing ride that shape).
//!
//! Writes here are **paths + commits only** — the precedence-marker guard
//! (6.5) stays in `spt-live::context`, which writes through the paths this
//! store hands out, then calls the commit hook. Single writer per branch
//! (active-instance-authoritative; 6.4).
// [impl->REQ-STORE-1]

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

use crate::branchstore::BranchStore;
use crate::perch::tracked_dir;
use spt_proto::id::validate_endpoint_id;

/// The per-agent live-context file inside an `a-<id>` worktree.
pub const LIVE_CONTEXT_FILE: &str = "live-context.md";
/// The per-(agent,project) context file inside a `p-<project>` worktree,
/// nested under the agent id.
pub const PROJECT_CONTEXT_FILE: &str = "project-context.md";

/// The branch carrying `id`'s live context (the per-agent view).
pub fn agent_branch(id: &str) -> String {
    format!("a-{id}")
}

/// The branch carrying `project_id`'s per-project context (all agents).
pub fn project_branch(project_id: &str) -> String {
    format!("p-{project_id}")
}

/// Pure path of `id`'s live-context file under a `tracked/` root — the
/// **read** form (no branch/worktree creation; readers must not mint store
/// state). The single layout authority both the ensure-paths and every reader
/// route through (6.1).
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn live_context_file(root: &Path, id: &str) -> PathBuf {
    root.join("agents").join(id).join(LIVE_CONTEXT_FILE)
}

/// Pure path of the per-(agent,project) context file under a `tracked/` root —
/// the **read** form (no creation).
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn project_context_file(root: &Path, project_id: &str, id: &str) -> PathBuf {
    root.join("projects").join(project_id).join(id).join(PROJECT_CONTEXT_FILE)
}

/// The two-tier context store rooted at a `tracked/` directory.
pub struct ContextStore {
    store: BranchStore,
    root: PathBuf,
}

impl ContextStore {
    /// Open (or initialize) the node's canonical store at
    /// [`tracked_dir`](crate::perch::tracked_dir).
    pub fn open_or_init() -> std::io::Result<ContextStore> {
        Self::open_or_init_in(&tracked_dir())
    }

    /// Open (or initialize) a store rooted at an explicit `tracked/` dir —
    /// the testable seam.
    // [impl->REQ-STORE-1]
    pub fn open_or_init_in(root: &Path) -> std::io::Result<ContextStore> {
        let store = BranchStore::open_or_init(&root.join(".seed.git"))?;
        Ok(ContextStore { store, root: root.to_path_buf() })
    }

    /// The underlying branch-as-KV store (D6c's bundle exchange works on it).
    pub fn branch_store(&self) -> &BranchStore {
        &self.store
    }

    /// The `tracked/` root this store lives under.
    pub fn root(&self) -> &Path {
        &self.root
    }

    fn validated(id: &str) -> std::io::Result<()> {
        validate_endpoint_id(id)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))
    }

    /// Ensure the `a-<id>` branch + worktree exist; return the live-context
    /// file path inside it (`tracked/agents/<id>/live-context.md`). The id is
    /// charset-validated (4.6) — branch names and worktree dirs are derived
    /// from it.
    // [impl->REQ-STORE-1]
    pub fn live_context_path(&self, id: &str) -> std::io::Result<PathBuf> {
        Self::validated(id)?;
        let file = live_context_file(&self.root, id);
        let wt = file.parent().expect("live-context file has a worktree parent");
        self.store.ensure_worktree(&agent_branch(id), wt)?;
        Ok(file)
    }

    /// Ensure the `p-<project_id>` branch + worktree exist; return the
    /// per-(agent,project) context file path inside it
    /// (`tracked/projects/<project_id>/<id>/project-context.md`). Both
    /// components are charset-validated (4.6; project ids come from
    /// [`crate::project::project_id_for_dir`], which emits the same charset).
    // [impl->REQ-STORE-1]
    pub fn project_context_path(&self, project_id: &str, id: &str) -> std::io::Result<PathBuf> {
        Self::validated(project_id)?;
        Self::validated(id)?;
        let file = project_context_file(&self.root, project_id, id);
        let wt = self.root.join("projects").join(project_id);
        self.store.ensure_worktree(&project_branch(project_id), &wt)?;
        std::fs::create_dir_all(file.parent().expect("project-context file has an agent dir"))?;
        Ok(file)
    }

    /// Commit `id`'s live-context worktree — one checkpoint commit on `a-<id>`
    /// (no-op when nothing changed). Call after a guarded write through
    /// [`live_context_path`](Self::live_context_path).
    // [impl->REQ-STORE-1]
    pub fn commit_live(&self, id: &str, message: &str) -> std::io::Result<Option<String>> {
        Self::validated(id)?;
        let wt = self.root.join("agents").join(id);
        self.store.commit_in_worktree(&wt, &[], message)
    }

    /// Commit the `p-<project_id>` worktree — one checkpoint commit (no-op
    /// when nothing changed).
    // [impl->REQ-STORE-1]
    pub fn commit_project(&self, project_id: &str, message: &str) -> std::io::Result<Option<String>> {
        Self::validated(project_id)?;
        let wt = self.root.join("projects").join(project_id);
        self.store.commit_in_worktree(&wt, &[], message)
    }
}

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

    #[test] // [unit->REQ-STORE-1] tier paths land on the documented layout; commits checkpoint per branch.
    fn two_tier_layout_and_commits() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();

        let live = cs.live_context_path("doyle").unwrap();
        assert!(live.ends_with(Path::new("agents").join("doyle").join(LIVE_CONTEXT_FILE)));
        std::fs::write(&live, "live mind").unwrap();
        let c = cs.commit_live("doyle", "live v1").unwrap();
        assert!(c.is_some());
        assert_eq!(
            cs.branch_store().read_at_tip(&agent_branch("doyle"), LIVE_CONTEXT_FILE).unwrap(),
            Some(b"live mind".to_vec())
        );

        let proj = cs.project_context_path("spt-core", "doyle").unwrap();
        assert!(proj.ends_with(
            Path::new("projects").join("spt-core").join("doyle").join(PROJECT_CONTEXT_FILE)
        ));
        std::fs::write(&proj, "project mind").unwrap();
        let c = cs.commit_project("spt-core", "proj v1").unwrap();
        assert!(c.is_some());
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&project_branch("spt-core"), "doyle/project-context.md")
                .unwrap(),
            Some(b"project mind".to_vec())
        );
    }

    #[test] // [unit->REQ-STORE-1] the p- branch aggregates multiple agents (cross-agent synthesis shape).
    fn project_branch_holds_many_agents() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        for id in ["doyle", "momo"] {
            let p = cs.project_context_path("spt-core", id).unwrap();
            std::fs::write(&p, format!("{id} ctx")).unwrap();
        }
        cs.commit_project("spt-core", "both").unwrap().unwrap();
        let bs = cs.branch_store();
        assert!(bs.read_at_tip(&project_branch("spt-core"), "doyle/project-context.md").unwrap().is_some());
        assert!(bs.read_at_tip(&project_branch("spt-core"), "momo/project-context.md").unwrap().is_some());
    }

    #[test] // [unit->REQ-HAZARD-ID-CHARSET] a path-smuggling id is refused before any branch/dir derives from it.
    fn invalid_id_refused() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        for bad in ["../up", "a/b", "a:b", "a@b", ""] {
            assert!(cs.live_context_path(bad).is_err(), "{bad:?} must refuse");
        }
    }
}

+    /// The context-store arm of `spt rename <old> <new>` (REQ-INST-11, the

+    /// D3f-documented seam): move the `a-<old>` branch + its worktree to

+    /// `a-<new>`, and rename `<old>/` → `<new>/` inside every project worktree

+    /// (one rename commit per touched project). Refuses when `a-<new>` already

+    /// exists (never merge two minds); an absent `a-<old>` is a no-op `Ok`

+    /// (nothing tracked yet — perch-only endpoints rename cleanly).

+    pub fn rename_endpoint(&self, old_id: &str, new_id: &str) -> std::io::Result<()> {

+        if self.store.has_branch(&agent_branch(new_id))? {

+            return Err(std::io::Error::new(

+                std::io::ErrorKind::AlreadyExists,

+                format!("context branch already exists: {}", agent_branch(new_id)),

+        // a- arm: move worktree dir, then rename the branch (git updates the

+        // worktree's HEAD symref). Skip when the old branch never existed.

+        if self.store.has_branch(&agent_branch(old_id))? {

+            let old_wt = self.root.join("agents").join(old_id);

+            let new_wt = self.root.join("agents").join(new_id);

+            if old_wt.join(".git").exists() {

+                crate::gitrun::run_git_ok(

+                    &["worktree", "move", &old_wt.to_string_lossy(), &new_wt.to_string_lossy()],

+                    Some(&old_wt.parent().expect("agents/ parent")),

+                &["branch", "-m", &agent_branch(old_id), &agent_branch(new_id)],

+        // p- arm: rename the agent dir inside every project worktree holding one.

+        let projects = self.root.join("projects");

+        if let Ok(entries) = std::fs::read_dir(&projects) {

+            for entry in entries.flatten() {

+                if !wt.join(".git").exists() || !wt.join(old_id).exists() {

+                crate::gitrun::run_git_ok(&["mv", old_id, new_id], Some(&wt), None)?;

+                self.store.commit_in_worktree(

+                    &format!("rename {old_id} -> {new_id}"),


            if old_wt.join(".git").exists() {
                crate::gitrun::run_git_ok(
                    &["worktree", "move", &old_wt.to_string_lossy(), &new_wt.to_string_lossy()],
                    Some(&old_wt.parent().expect("agents/ parent")),
                    None,
                )?;
            }

            if old_wt.join(".git").exists() {
                crate::gitrun::run_git_ok(
                    &[
                        "-C",
                        &self.root.join(".seed.git").to_string_lossy(),
                        "worktree",
                        "move",
                        &old_wt.to_string_lossy(),
                        &new_wt.to_string_lossy(),
                    ],
                    None,
                    None,
                )?;
            }


            if old_wt.join(".git").exists() {
                crate::gitrun::run_git_ok(
                    &["worktree", "move", &old_wt.to_string_lossy(), &new_wt.to_string_lossy()],
                    Some(&old_wt.parent().expect("agents/ parent")),
                    None,
                )?;
            }

            if old_wt.join(".git").exists() {
                crate::gitrun::run_git_ok(
                    &[
                        "-C",
                        &self.root.join(".seed.git").to_string_lossy(),
                        "worktree",
                        "move",
                        &old_wt.to_string_lossy(),
                        &new_wt.to_string_lossy(),
                    ],
                    None,
                    None,
                )?;
            }

let new_wt = self.root.join("agents").join(new_id);

-                    &["worktree", "move", &old_wt.to_string_lossy(), &new_wt.to_string_lossy()],

-                    Some(&old_wt.parent().expect("agents/ parent")),

+                        &self.root.join(".seed.git").to_string_lossy(),

+                        &old_wt.to_string_lossy(),

+                        &new_wt.to_string_lossy(),


            crate::gitrun::run_git_ok(
                &["branch", "-m", &agent_branch(old_id), &agent_branch(new_id)],
                Some(&new_wt),
                None,
            )?;

            crate::gitrun::run_git_ok(
                &[
                    "-C",
                    &self.root.join(".seed.git").to_string_lossy(),
                    "branch",
                    "-m",
                    &agent_branch(old_id),
                    &agent_branch(new_id),
                ],
                None,
                None,
            )?;


            crate::gitrun::run_git_ok(
                &["branch", "-m", &agent_branch(old_id), &agent_branch(new_id)],
                Some(&new_wt),
                None,
            )?;

            crate::gitrun::run_git_ok(
                &[
                    "-C",
                    &self.root.join(".seed.git").to_string_lossy(),
                    "branch",
                    "-m",
                    &agent_branch(old_id),
                    &agent_branch(new_id),
                ],
                None,
                None,
            )?;

-                &["branch", "-m", &agent_branch(old_id), &agent_branch(new_id)],

+                    &self.root.join(".seed.git").to_string_lossy(),

+                    &agent_branch(old_id),

+                    &agent_branch(new_id),


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.


Add a rename test, then rebuild spt-store:


    #[test] // [unit->REQ-HAZARD-ID-CHARSET] a path-smuggling id is refused before any branch/dir derives from it.
    fn invalid_id_refused() {

    #[test] // [unit->REQ-INST-11] rename moves the a- branch + worktree and ripples every p- agent dir; target collision refuses.
    fn rename_ripples_both_tiers() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        let live = cs.live_context_path("doyle").unwrap();
        std::fs::write(&live, "mind").unwrap();
        cs.commit_live("doyle", "v1").unwrap().unwrap();
        let proj = cs.project_context_path("spt-core", "doyle").unwrap();
        std::fs::write(&proj, "proj mind").unwrap();
        cs.commit_project("spt-core", "v1").unwrap().unwrap();

        cs.rename_endpoint("doyle", "arthur").unwrap();

        // a- arm: branch + worktree moved, content intact, old gone.
        assert!(cs.branch_store().has_branch(&agent_branch("arthur")).unwrap());
        assert!(!cs.branch_store().has_branch(&agent_branch("doyle")).unwrap());
        assert_eq!(
            std::fs::read_to_string(live_context_file(tmp.path(), "arthur")).unwrap(),
            "mind"
        );
        assert!(!tmp.path().join("agents").join("doyle").exists());

        // p- arm: agent dir renamed inside the project worktree + committed.
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&project_branch("spt-core"), "arthur/project-context.md")
                .unwrap(),
            Some(b"proj mind".to_vec())
        );
        assert!(cs
            .branch_store()
            .read_at_tip(&project_branch("spt-core"), "doyle/project-context.md")
            .unwrap()
            .is_none());

        // Collision: an existing target mind refuses (never merge two minds).
        cs.live_context_path("momo").unwrap();
        let err = cs.rename_endpoint("arthur", "momo").unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);

        // Absent source: a perch-only endpoint renames as a clean no-op.
        cs.rename_endpoint("ghost", "phantom").unwrap();
    }

    #[test] // [unit->REQ-HAZARD-ID-CHARSET] a path-smuggling id is refused before any branch/dir derives from it.
    fn invalid_id_refused() {


    #[test] // [unit->REQ-INST-11] rename moves the a- branch + worktree and ripples every p- agent dir; target collision refuses.
    fn rename_ripples_both_tiers() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        let live = cs.live_context_path("doyle").unwrap();
        std::fs::write(&live, "mind").unwrap();
        cs.commit_live("doyle", "v1").unwrap().unwrap();
        let proj = cs.project_context_path("spt-core", "doyle").unwrap();
        std::fs::write(&proj, "proj mind").unwrap();
        cs.commit_project("spt-core", "v1").unwrap().unwrap();

        cs.rename_endpoint("doyle", "arthur").unwrap();

        // a- arm: branch + worktree moved, content intact, old gone.
        assert!(cs.branch_store().has_branch(&agent_branch("arthur")).unwrap());
        assert!(!cs.branch_store().has_branch(&agent_branch("doyle")).unwrap());
        assert_eq!(
            std::fs::read_to_string(live_context_file(tmp.path(), "arthur")).unwrap(),
            "mind"
        );
        assert!(!tmp.path().join("agents").join("doyle").exists());

        // p- arm: agent dir renamed inside the project worktree + committed.
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&project_branch("spt-core"), "arthur/project-context.md")
                .unwrap(),
            Some(b"proj mind".to_vec())
        );
        assert!(cs
            .branch_store()
            .read_at_tip(&project_branch("spt-core"), "doyle/project-context.md")
            .unwrap()
            .is_none());

        // Collision: an existing target mind refuses (never merge two minds).
        cs.live_context_path("momo").unwrap();
        let err = cs.rename_endpoint("arthur", "momo").unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);

        // Absent source: a perch-only endpoint renames as a clean no-op.
        cs.rename_endpoint("ghost", "phantom").unwrap();
    }

    #[test] // [unit->REQ-HAZARD-ID-CHARSET] a path-smuggling id is refused before any branch/dir derives from it.
    fn invalid_id_refused() {

+    #[test] // [unit->REQ-INST-11] rename moves the a- branch + worktree and ripples every p- agent dir; target collision refuses.

+        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();

+        let live = cs.live_context_path("doyle").unwrap();

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

+        cs.commit_live("doyle", "v1").unwrap().unwrap();

+        let proj = cs.project_context_path("spt-core", "doyle").unwrap();

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

+        cs.commit_project("spt-core", "v1").unwrap().unwrap();

+        cs.rename_endpoint("doyle", "arthur").unwrap();

+        // a- arm: branch + worktree moved, content intact, old gone.

+        assert!(cs.branch_store().has_branch(&agent_branch("arthur")).unwrap());

+        assert!(!cs.branch_store().has_branch(&agent_branch("doyle")).unwrap());

+            std::fs::read_to_string(live_context_file(tmp.path(), "arthur")).unwrap(),

+        assert!(!tmp.path().join("agents").join("doyle").exists());

+        // p- arm: agent dir renamed inside the project worktree + committed.

+                .read_at_tip(&project_branch("spt-core"), "arthur/project-context.md")

+            Some(b"proj mind".to_vec())

+            .read_at_tip(&project_branch("spt-core"), "doyle/project-context.md")

+        // Collision: an existing target mind refuses (never merge two minds).

+        cs.live_context_path("momo").unwrap();

+        let err = cs.rename_endpoint("arthur", "momo").unwrap_err();

+        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);

+        // Absent source: a perch-only endpoint renames as a clean no-op.

+        cs.rename_endpoint("ghost", "phantom").unwrap();


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-store --lib contextstore 2>&1 | tail -10

Test contextstore additions


     Running unittests src\lib.rs (target\debug\deps\spt_store-48f4f027db936eae.exe)

running 4 tests
test contextstore::tests::invalid_id_refused ... ok
test contextstore::tests::project_branch_holds_many_agents ... ok
test contextstore::tests::two_tier_layout_and_commits ... ok
test contextstore::tests::rename_ripples_both_tiers ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 1.15s


<task-notification>
<task-id>bwfbkkgia</task-id>
<tool-use-id>toolu_01MFY59gnmb811wnL2sLwmHi</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\bwfbkkgia.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI until 8b42452 completes" completed (exit code 0)</summary>
</task-notification>


Now the spt-live rewrite — ingest tier routing:


Write

//! drop-file ingest (M2b T4, KNOWN-HAZARDS 6.4) — two-tier routed since M4-D6a.
//!
//! The harness-invoked **mind** (a Psyche / echo session) and the **operator**
//! drop context into the manifest's watched `commune_dir` / `signoff_dir` as
//! `<id>-commune.md` / `<id>-signoff.md`. spt-core is the **single writer**: it
//! ingests each drop and then **deletes** it. The mind is read-only on drop
//! files; it never deletes (6.4 — the removed-delete fix). Every write is
//! precedence-guarded (6.5, [`crate::context`]) because a dropped commune is an
//! LLM snapshot.
//!
//! ## Two-tier routing (D6a, REQ-INST-2 / CONTEXT-MEMORY)
//! A drop body is parsed with the two-slice envelope grammar
//! ([`spt_proto::envelope::parse_two_slice`]):
//! - `<live-context>` → `tracked/agents/<id>/live-context.md` (branch `a-<id>`,
//!   syncs to **all** instances);
//! - `<project-context>` → `tracked/projects/<project_id>/<id>/project-context.md`
//!   (branch `p-<project_id>`, syncs to **same-project** instances only);
//! - an untagged body routes whole to **live** (the parser's fallback — a bare
//!   operator note is cross-project by default).
//!
//! Each guarded write is followed by a BranchStore checkpoint commit
//! (ADR-0011: commit = checkpoint); a suppressed write changes nothing and
//! commits nothing. The old per-perch flat `context.md` is gone — the tracked
//! store is the one context location (6.1).
//
// [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER]
// [impl->REQ-API-3]
// [impl->REQ-INST-2]

use crate::context::{write_context, WriteOutcome, WriteSource};
use spt_proto::envelope::parse_two_slice;
use spt_store::contextstore::ContextStore;
use std::path::{Path, PathBuf};

/// Fixed drop-file suffixes (spt-core owns the filenames; the adapter owns only
/// the directory — MANIFEST §watched dirs).
pub const COMMUNE_SUFFIX: &str = "-commune.md";
pub const SIGNOFF_SUFFIX: &str = "-signoff.md";

/// Which kind of drop was ingested.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropKind {
    Commune,
    Signoff,
}

/// Which context tier a routed write landed in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier {
    Live,
    Project,
}

/// One tier-routed guarded write.
#[derive(Debug, Clone)]
pub struct TierWrite {
    pub tier: Tier,
    pub outcome: WriteOutcome,
}

/// One ingested drop: its kind, the tier writes it produced, and the (now
/// deleted) source path.
#[derive(Debug, Clone)]
pub struct Ingested {
    pub kind: DropKind,
    pub writes: Vec<TierWrite>,
    pub drop_path: PathBuf,
}

/// Route one drop/commune body into the two-tier context store: parse the
/// slices, guarded-write each non-empty one into its tier file, checkpoint the
/// touched branch. Returns the writes performed (empty when both slices were
/// blank — a contentless drop routes nowhere).
// [impl->REQ-INST-2]
pub fn route_two_slice(
    id: &str,
    project_id: &str,
    body: &str,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<TierWrite>> {
    let slices = parse_two_slice(body);
    route_slices(
        id,
        project_id,
        slices.live.as_deref(),
        slices.project.as_deref(),
        now_ms,
        window_ms,
    )
}

/// [`route_two_slice`] over pre-split (and possibly pre-stamped) slices — the
/// seam [`crate::signoff::write_resume_commune`] feeds provenance-stamped
/// bodies through.
// [impl->REQ-INST-2]
pub fn route_slices(
    id: &str,
    project_id: &str,
    live: Option<&str>,
    project: Option<&str>,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<TierWrite>> {
    let cs = ContextStore::open_or_init()?;
    let mut writes = Vec::new();

    if let Some(body) = live.filter(|b| !b.trim().is_empty()) {
        let path = cs.live_context_path(id)?;
        let outcome = write_context(&path, body, WriteSource::Llm, now_ms, window_ms)?;
        if matches!(outcome, WriteOutcome::Written { .. }) {
            cs.commit_live(id, &format!("commune: live ({id})"))?;
        }
        writes.push(TierWrite { tier: Tier::Live, outcome });
    }
    if let Some(body) = project.filter(|b| !b.trim().is_empty()) {
        let path = cs.project_context_path(project_id, id)?;
        let outcome = write_context(&path, body, WriteSource::Llm, now_ms, window_ms)?;
        if matches!(outcome, WriteOutcome::Written { .. }) {
            cs.commit_project(project_id, &format!("commune: project ({id})"))?;
        }
        writes.push(TierWrite { tier: Tier::Project, outcome });
    }
    Ok(writes)
}

/// Ingest any `<id>-commune.md` / `<id>-signoff.md` present in `drop_dir` for
/// `id`: route each into the two-tier context store (precedence-guarded, llm
/// source) and **delete** it (spt-core single-writer; the mind never deletes).
///
/// A drop is deleted whether its writes `Written` or `Suppressed` — both mean
/// "consumed" (a suppressed drop was a stale snapshot, correctly discarded). A
/// read/write *error* leaves the drop in place to retry on the next pass.
pub fn ingest_drops(
    drop_dir: &Path,
    id: &str,
    project_id: &str,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<Ingested>> {
    let mut out = Vec::new();
    for (kind, suffix) in [
        (DropKind::Commune, COMMUNE_SUFFIX),
        (DropKind::Signoff, SIGNOFF_SUFFIX),
    ] {
        let drop_path = drop_dir.join(format!("{id}{suffix}"));
        if !drop_path.exists() {
            continue;
        }
        let body = std::fs::read_to_string(&drop_path)?;
        // A dropped commune/signoff is an LLM-authored snapshot.
        let writes = route_two_slice(id, project_id, &body, now_ms, window_ms)?;
        // [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core deletes the drop
        // after consuming it; the mind never does.
        std::fs::remove_file(&drop_path)?;
        out.push(Ingested {
            kind,
            writes,
            drop_path,
        });
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::context::DEFAULT_PROTECTION_WINDOW_MS;
    use crate::test_support::with_home;
    use spt_store::contextstore::{live_context_file, project_context_file};
    use spt_store::perch::tracked_dir;

    fn read_live(id: &str) -> String {
        std::fs::read_to_string(live_context_file(&tracked_dir(), id)).unwrap_or_default()
    }
    fn read_project(pid: &str, id: &str) -> String {
        std::fs::read_to_string(project_context_file(&tracked_dir(), pid, id)).unwrap_or_default()
    }

    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] a dropped commune is ingested + deleted by spt-core.
    fn commune_drop_is_ingested_and_deleted() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let drop = drops.path().join("doyle-commune.md");
            std::fs::write(&drop, "Source: echo-commune\nthe brief").unwrap();

            let ingested = ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000)
                .expect("ingest");
            assert_eq!(ingested.len(), 1);
            assert_eq!(ingested[0].kind, DropKind::Commune);
            assert!(matches!(
                ingested[0].writes.as_slice(),
                [TierWrite { tier: Tier::Live, outcome: WriteOutcome::Written { .. } }]
            ));

            // spt-core deleted the drop (single-writer ingest→delete).
            assert!(!drop.exists(), "drop must be deleted after ingest");
            // An untagged body routed whole to the live tier.
            assert!(read_live("doyle").contains("the brief"));
        });
    }

    #[test] // [unit->REQ-INST-2] tagged slices route to their tier files + branches.
    fn two_slice_drop_routes_per_tier() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            std::fs::write(
                drops.path().join("doyle-commune.md"),
                "<live-context>\nwho I am\n</live-context>\n\
                 <project-context>\nwhat I do here\n</project-context>",
            )
            .unwrap();

            let ingested =
                ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000).expect("ingest");
            let tiers: Vec<Tier> = ingested[0].writes.iter().map(|w| w.tier).collect();
            assert_eq!(tiers, vec![Tier::Live, Tier::Project]);

            assert!(read_live("doyle").contains("who I am"));
            assert!(!read_live("doyle").contains("what I do here"), "no project leak into live");
            assert!(read_project("proj-x", "doyle").contains("what I do here"));

            // Both branches checkpointed (ADR-0011 commit=checkpoint).
            let cs = ContextStore::open_or_init().unwrap();
            assert!(cs.branch_store().tip("a-doyle").unwrap().is_some());
            assert!(cs.branch_store().tip("p-proj-x").unwrap().is_some());
        });
    }

    #[test] // [unit->REQ-API-3] commune + signoff drops both ingest in one pass.
    fn both_drops_ingested() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            std::fs::write(drops.path().join("doyle-commune.md"), "c").unwrap();
            std::fs::write(drops.path().join("doyle-signoff.md"), "s").unwrap();
            let ingested =
                ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000).expect("ingest");
            let kinds: Vec<DropKind> = ingested.iter().map(|i| i.kind).collect();
            assert!(kinds.contains(&DropKind::Commune) && kinds.contains(&DropKind::Signoff));
            assert!(!drops.path().join("doyle-commune.md").exists());
            assert!(!drops.path().join("doyle-signoff.md").exists());
        });
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] a stale commune within a direct window is suppressed but still consumed.
    fn stale_commune_suppressed_but_deleted() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            // Seed a fresh direct write into the live tier first.
            let cs = ContextStore::open_or_init().unwrap();
            let live = cs.live_context_path("doyle").unwrap();
            write_context(&live, "DIRECT", WriteSource::Direct, 1000, DEFAULT_PROTECTION_WINDOW_MS)
                .unwrap();

            // A commune drop landing within the window must be suppressed.
            let drop = drops.path().join("doyle-commune.md");
            std::fs::write(&drop, "STALE COMMUNE").unwrap();
            let ingested =
                ingest_drops(drops.path(), "doyle", "proj-x", 1500, DEFAULT_PROTECTION_WINDOW_MS)
                    .expect("ingest");
            assert!(matches!(
                ingested[0].writes[0].outcome,
                WriteOutcome::Suppressed { .. }
            ));
            // Still consumed (deleted) and the direct write preserved.
            assert!(!drop.exists());
            let after = read_live("doyle");
            assert!(after.contains("DIRECT") && !after.contains("STALE COMMUNE"));
        });
    }

    #[test] // no drops present ⇒ a clean empty pass, no error.
    fn no_drops_is_empty() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let ingested =
                ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000).expect("ingest");
            assert!(ingested.is_empty());
        });
    }
}


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

//! drop-file ingest (M2b T4, KNOWN-HAZARDS 6.4) — two-tier routed since M4-D6a.
//!
//! The harness-invoked **mind** (a Psyche / echo session) and the **operator**
//! drop context into the manifest's watched `commune_dir` / `signoff_dir` as
//! `<id>-commune.md` / `<id>-signoff.md`. spt-core is the **single writer**: it
//! ingests each drop and then **deletes** it. The mind is read-only on drop
//! files; it never deletes (6.4 — the removed-delete fix). Every write is
//! precedence-guarded (6.5, [`crate::context`]) because a dropped commune is an
//! LLM snapshot.
//!
//! ## Two-tier routing (D6a, REQ-INST-2 / CONTEXT-MEMORY)
//! A drop body is parsed with the two-slice envelope grammar
//! ([`spt_proto::envelope::parse_two_slice`]):
//! - `<live-context>` → `tracked/agents/<id>/live-context.md` (branch `a-<id>`,
//!   syncs to **all** instances);
//! - `<project-context>` → `tracked/projects/<project_id>/<id>/project-context.md`
//!   (branch `p-<project_id>`, syncs to **same-project** instances only);
//! - an untagged body routes whole to **live** (the parser's fallback — a bare
//!   operator note is cross-project by default).
//!
//! Each guarded write is followed by a BranchStore checkpoint commit
//! (ADR-0011: commit = checkpoint); a suppressed write changes nothing and
//! commits nothing. The old per-perch flat `context.md` is gone — the tracked
//! store is the one context location (6.1).
//
// [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER]
// [impl->REQ-API-3]
// [impl->REQ-INST-2]

use crate::context::{write_context, WriteOutcome, WriteSource};
use spt_proto::envelope::parse_two_slice;
use spt_store::contextstore::ContextStore;
use std::path::{Path, PathBuf};

/// Fixed drop-file suffixes (spt-core owns the filenames; the adapter owns only
/// the directory — MANIFEST §watched dirs).
pub const COMMUNE_SUFFIX: &str = "-commune.md";
pub const SIGNOFF_SUFFIX: &str = "-signoff.md";

/// Which kind of drop was ingested.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropKind {
    Commune,
    Signoff,
}

/// Which context tier a routed write landed in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier {
    Live,
    Project,
}

/// One tier-routed guarded write.
#[derive(Debug, Clone)]
pub struct TierWrite {
    pub tier: Tier,
    pub outcome: WriteOutcome,
}

/// One ingested drop: its kind, the tier writes it produced, and the (now
/// deleted) source path.
#[derive(Debug, Clone)]
pub struct Ingested {
    pub kind: DropKind,
    pub writes: Vec<TierWrite>,
    pub drop_path: PathBuf,
}

/// Route one drop/commune body into the two-tier context store: parse the
/// slices, guarded-write each non-empty one into its tier file, checkpoint the
/// touched branch. Returns the writes performed (empty when both slices were
/// blank — a contentless drop routes nowhere).
// [impl->REQ-INST-2]
pub fn route_two_slice(
    id: &str,
    project_id: &str,
    body: &str,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<TierWrite>> {
    let slices = parse_two_slice(body);
    route_slices(
        id,
        project_id,
        slices.live.as_deref(),
        slices.project.as_deref(),
        now_ms,
        window_ms,
    )
}

/// [`route_two_slice`] over pre-split (and possibly pre-stamped) slices — the
/// seam [`crate::signoff::write_resume_commune`] feeds provenance-stamped
/// bodies through.
// [impl->REQ-INST-2]
pub fn route_slices(
    id: &str,
    project_id: &str,
    live: Option<&str>,
    project: Option<&str>,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<TierWrite>> {
    let cs = ContextStore::open_or_init()?;
    let mut writes = Vec::new();

    if let Some(body) = live.filter(|b| !b.trim().is_empty()) {
        let path = cs.live_context_path(id)?;
        let outcome = write_context(&path, body, WriteSource::Llm, now_ms, window_ms)?;
        if matches!(outcome, WriteOutcome::Written { .. }) {
            cs.commit_live(id, &format!("commune: live ({id})"))?;
        }
        writes.push(TierWrite { tier: Tier::Live, outcome });
    }
    if let Some(body) = project.filter(|b| !b.trim().is_empty()) {
        let path = cs.project_context_path(project_id, id)?;
        let outcome = write_context(&path, body, WriteSource::Llm, now_ms, window_ms)?;
        if matches!(outcome, WriteOutcome::Written { .. }) {
            cs.commit_project(project_id, &format!("commune: project ({id})"))?;
        }
        writes.push(TierWrite { tier: Tier::Project, outcome });
    }
    Ok(writes)
}

/// Ingest any `<id>-commune.md` / `<id>-signoff.md` present in `drop_dir` for
/// `id`: route each into the two-tier context store (precedence-guarded, llm
/// source) and **delete** it (spt-core single-writer; the mind never deletes).
///
/// A drop is deleted whether its writes `Written` or `Suppressed` — both mean
/// "consumed" (a suppressed drop was a stale snapshot, correctly discarded). A
/// read/write *error* leaves the drop in place to retry on the next pass.
pub fn ingest_drops(
    drop_dir: &Path,
    id: &str,
    project_id: &str,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<Ingested>> {
    let mut out = Vec::new();
    for (kind, suffix) in [
        (DropKind::Commune, COMMUNE_SUFFIX),
        (DropKind::Signoff, SIGNOFF_SUFFIX),
    ] {
        let drop_path = drop_dir.join(format!("{id}{suffix}"));
        if !drop_path.exists() {
            continue;
        }
        let body = std::fs::read_to_string(&drop_path)?;
        // A dropped commune/signoff is an LLM-authored snapshot.
        let writes = route_two_slice(id, project_id, &body, now_ms, window_ms)?;
        // [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core deletes the drop
        // after consuming it; the mind never does.
        std::fs::remove_file(&drop_path)?;
        out.push(Ingested {
            kind,
            writes,
            drop_path,
        });
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::context::DEFAULT_PROTECTION_WINDOW_MS;
    use crate::test_support::with_home;
    use spt_store::contextstore::{live_context_file, project_context_file};
    use spt_store::perch::tracked_dir;

    fn read_live(id: &str) -> String {
        std::fs::read_to_string(live_context_file(&tracked_dir(), id)).unwrap_or_default()
    }
    fn read_project(pid: &str, id: &str) -> String {
        std::fs::read_to_string(project_context_file(&tracked_dir(), pid, id)).unwrap_or_default()
    }

    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] a dropped commune is ingested + deleted by spt-core.
    fn commune_drop_is_ingested_and_deleted() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let drop = drops.path().join("doyle-commune.md");
            std::fs::write(&drop, "Source: echo-commune\nthe brief").unwrap();

            let ingested = ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000)
                .expect("ingest");
            assert_eq!(ingested.len(), 1);
            assert_eq!(ingested[0].kind, DropKind::Commune);
            assert!(matches!(
                ingested[0].writes.as_slice(),
                [TierWrite { tier: Tier::Live, outcome: WriteOutcome::Written { .. } }]
            ));

            // spt-core deleted the drop (single-writer ingest→delete).
            assert!(!drop.exists(), "drop must be deleted after ingest");
            // An untagged body routed whole to the live tier.
            assert!(read_live("doyle").contains("the brief"));
        });
    }

    #[test] // [unit->REQ-INST-2] tagged slices route to their tier files + branches.
    fn two_slice_drop_routes_per_tier() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            std::fs::write(
                drops.path().join("doyle-commune.md"),
                "<live-context>\nwho I am\n</live-context>\n\
                 <project-context>\nwhat I do here\n</project-context>",
            )
            .unwrap();

            let ingested =
                ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000).expect("ingest");
            let tiers: Vec<Tier> = ingested[0].writes.iter().map(|w| w.tier).collect();
            assert_eq!(tiers, vec![Tier::Live, Tier::Project]);

            assert!(read_live("doyle").contains("who I am"));
            assert!(!read_live("doyle").contains("what I do here"), "no project leak into live");
            assert!(read_project("proj-x", "doyle").contains("what I do here"));

            // Both branches checkpointed (ADR-0011 commit=checkpoint).
            let cs = ContextStore::open_or_init().unwrap();
            assert!(cs.branch_store().tip("a-doyle").unwrap().is_some());
            assert!(cs.branch_store().tip("p-proj-x").unwrap().is_some());
        });
    }

    #[test] // [unit->REQ-API-3] commune + signoff drops both ingest in one pass.
    fn both_drops_ingested() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            std::fs::write(drops.path().join("doyle-commune.md"), "c").unwrap();
            std::fs::write(drops.path().join("doyle-signoff.md"), "s").unwrap();
            let ingested =
                ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000).expect("ingest");
            let kinds: Vec<DropKind> = ingested.iter().map(|i| i.kind).collect();
            assert!(kinds.contains(&DropKind::Commune) && kinds.contains(&DropKind::Signoff));
            assert!(!drops.path().join("doyle-commune.md").exists());
            assert!(!drops.path().join("doyle-signoff.md").exists());
        });
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] a stale commune within a direct window is suppressed but still consumed.
    fn stale_commune_suppressed_but_deleted() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            // Seed a fresh direct write into the live tier first.
            let cs = ContextStore::open_or_init().unwrap();
            let live = cs.live_context_path("doyle").unwrap();
            write_context(&live, "DIRECT", WriteSource::Direct, 1000, DEFAULT_PROTECTION_WINDOW_MS)
                .unwrap();

            // A commune drop landing within the window must be suppressed.
            let drop = drops.path().join("doyle-commune.md");
            std::fs::write(&drop, "STALE COMMUNE").unwrap();
            let ingested =
                ingest_drops(drops.path(), "doyle", "proj-x", 1500, DEFAULT_PROTECTION_WINDOW_MS)
                    .expect("ingest");
            assert!(matches!(
                ingested[0].writes[0].outcome,
                WriteOutcome::Suppressed { .. }
            ));
            // Still consumed (deleted) and the direct write preserved.
            assert!(!drop.exists());
            let after = read_live("doyle");
            assert!(after.contains("DIRECT") && !after.contains("STALE COMMUNE"));
        });
    }

    #[test] // no drops present ⇒ a clean empty pass, no error.
    fn no_drops_is_empty() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let ingested =
                ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000).expect("ingest");
            assert!(ingested.is_empty());
        });
    }
}

-//! drop-file ingest (M2b T4, KNOWN-HAZARDS 6.4).

+//! drop-file ingest (M2b T4, KNOWN-HAZARDS 6.4) — two-tier routed since M4-D6a.

-//! ingests each drop into the perch's context store and then **deletes** it. The

-//! mind is read-only on drop files; it never deletes (6.4 — the removed-delete

-//! fix). The write is precedence-guarded (6.5, [`crate::context`]) because a

-//! dropped commune is an LLM snapshot.

+//! ingests each drop and then **deletes** it. The mind is read-only on drop

+//! files; it never deletes (6.4 — the removed-delete fix). Every write is

+//! precedence-guarded (6.5, [`crate::context`]) because a dropped commune is an

+//! ## Two-tier routing (D6a, REQ-INST-2 / CONTEXT-MEMORY)

+//! A drop body is parsed with the two-slice envelope grammar

+//! - `<live-context>` → `tracked/agents/<id>/live-context.md` (branch `a-<id>`,

+//! - `<project-context>` → `tracked/projects/<project_id>/<id>/project-context.md`

+//!   (branch `p-<project_id>`, syncs to **same-project** instances only);

+//! - an untagged body routes whole to **live** (the parser's fallback — a bare

+//!   operator note is cross-project by default).

+//! Each guarded write is followed by a BranchStore checkpoint commit

+//! (ADR-0011: commit = checkpoint); a suppressed write changes nothing and

+//! commits nothing. The old per-perch flat `context.md` is gone — the tracked

+//! store is the one context location (6.1).

-// Clean-room over the M2a seams; the watch/poll cadence is the pulse (T5) — this

-// module is the per-pass ingest mechanism it drives.

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

/// the directory — MANIFEST §watched dirs).

pub const COMMUNE_SUFFIX: &str = "-commune.md";

pub const SIGNOFF_SUFFIX: &str = "-signoff.md";

-/// The per-perch context store file ingested drops fold into.

-pub const CONTEXT_FILE: &str = "context.md";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]

-/// One ingested drop: its kind, the guarded-write outcome, and the (now deleted)

+/// Which context tier a routed write landed in.

+#[derive(Debug, Clone, Copy, PartialEq, Eq)]

+/// One ingested drop: its kind, the tier writes it produced, and the (now

-pub fn context_path(id: &str, hint: ParentHint<'_>) -> PathBuf {

-    resolve_perch_path(id, hint).join(CONTEXT_FILE)

+/// Route one drop/commune body into the two-tier context store: parse the

+/// slices, guarded-write each non-empty one into its tier file, checkpoint the

+/// touched branch. Returns the writes performed (empty when both slices were

+/// blank — a contentless drop routes nowhere).

+    let slices = parse_two_slice(body);

+/// [`route_two_slice`] over pre-split (and possibly pre-stamped) slices — the

+/// seam [`crate::signoff::write_resume_commune`] feeds provenance-stamped

+    let cs = ContextStore::open_or_init()?;

+    if let Some(body) = live.filter(|b| !b.trim().is_empty()) {

+        let path = cs.live_context_path(id)?;

+        let outcome = write_context(&path, body, WriteSource::Llm, now_ms, window_ms)?;

+        if matches!(outcome, WriteOutcome::Written { .. }) {

+            cs.commit_live(id, &format!("commune: live ({id})"))?;

+        writes.push(TierWrite { tier: Tier::Live, outcome });

+    if let Some(body) = project.filter(|b| !b.trim().is_empty()) {

+        let path = cs.project_context_path(project_id, id)?;

+            cs.commit_project(project_id, &format!("commune: project ({id})"))?;

+        writes.push(TierWrite { tier: Tier::Project, outcome });

-/// `id`: fold each into the perch's context store (precedence-guarded, llm

+/// `id`: route each into the two-tier context store (precedence-guarded, llm

-/// A drop is deleted whether its write `Written` or `Suppressed` — both mean

+/// A drop is deleted whether its writes `Written` or `Suppressed` — both mean

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

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

let body = std::fs::read_to_string(&drop_path)?;

// A dropped commune/signoff is an LLM-authored snapshot.

-        let outcome = write_context(&ctx, &body, WriteSource::Llm, now_ms, window_ms)?;

+        let writes = route_two_slice(id, project_id, &body, now_ms, window_ms)?;

// after consuming it; the mind never does.

+    use spt_store::contextstore::{live_context_file, project_context_file};

+        std::fs::read_to_string(live_context_file(&tracked_dir(), id)).unwrap_or_default()

+    fn read_project(pid: &str, id: &str) -> String {

+        std::fs::read_to_string(project_context_file(&tracked_dir(), pid, id)).unwrap_or_default()

let drop = drops.path().join("doyle-commune.md");

std::fs::write(&drop, "Source: echo-commune\nthe brief").unwrap();

-            let ingested = ingest_drops(drops.path(), "doyle", ParentHint::Infer, 1000, 60_000)

+            let ingested = ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000)

-            assert!(matches!(ingested[0].outcome, WriteOutcome::Written { .. }));

+                ingested[0].writes.as_slice(),

+                [TierWrite { tier: Tier::Live, outcome: WriteOutcome::Written { .. } }]

// spt-core deleted the drop (single-writer ingest→delete).

assert!(!drop.exists(), "drop must be deleted after ingest");

-            // Content folded into the perch's context store.

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

-            assert!(std::fs::read_to_string(&ctx).unwrap().contains("the brief"));

+            // An untagged body routed whole to the live tier.

+            assert!(read_live("doyle").contains("the brief"));

+    #[test] // [unit->REQ-INST-2] tagged slices route to their tier files + branches.

+    fn two_slice_drop_routes_per_tier() {

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

+                drops.path().join("doyle-commune.md"),

+                "<live-context>\nwho I am\n</live-context>\n\

+                 <project-context>\nwhat I do here\n</project-context>",

+                ingest_drops(drops.path(), "doyle", "proj-x", 1000, 60_000).expect("ingest");

+            let tiers: Vec<Tier> = ingested[0].writes.iter().map(|w| w.tier).collect();

+            assert_eq!(tiers, vec![Tier::Live, Tier::Project]);

+            assert!(read_live("doyle").contains("who I am"));

+            assert!(!read_live("doyle").contains("what I do here"), "no project leak into live");

+            assert!(read_project("proj-x", "doyle").contains("what I do here"));

+            // Both branches checkpointed (ADR-0011 commit=checkpoint).

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

+            assert!(cs.branch_store().tip("a-doyle").unwrap().is_some());

+            assert!(cs.branch_store().tip("p-proj-x").unwrap().is_some());

let drops = tempfile::tempdir().unwrap();

let kinds: Vec<DropKind> = ingested.iter().map(|i| i.kind).collect();

-            // Seed a fresh direct write into the context store first.

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

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

-            write_context(&ctx, "DIRECT", WriteSource::Direct, 1000, DEFAULT_PROTECTION_WINDOW_MS)

+            // Seed a fresh direct write into the live tier first.

+            let live = cs.live_context_path("doyle").unwrap();

+            write_context(&live, "DIRECT", WriteSource::Direct, 1000, DEFAULT_PROTECTION_WINDOW_MS)

// A commune drop landing within the window must be suppressed.

let drop = drops.path().join("doyle-commune.md");

-                ingest_drops(drops.path(), "doyle", ParentHint::Infer, 1500, DEFAULT_PROTECTION_WINDOW_MS)

+                ingest_drops(drops.path(), "doyle", "proj-x", 1500, DEFAULT_PROTECTION_WINDOW_MS)

-            assert!(matches!(ingested[0].outcome, WriteOutcome::Suppressed { .. }));

+                ingested[0].writes[0].outcome,

+                WriteOutcome::Suppressed { .. }

// Still consumed (deleted) and the direct write preserved.

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

+            let after = read_live("doyle");

assert!(after.contains("DIRECT") && !after.contains("STALE COMMUNE"));

let drops = tempfile::tempdir().unwrap();

-                ingest_drops(drops.path(), "doyle", ParentHint::Infer, 1000, 60_000).expect("ingest");

//! drop-file ingest (M2b T4, KNOWN-HAZARDS 6.4).
//!
//! The harness-invoked **mind** (a Psyche / echo session) and the **operator**
//! drop context into the manifest's watched `commune_dir` / `signoff_dir` as
//! `<id>-commune.md` / `<id>-signoff.md`. spt-core is the **single writer**: it
//! ingests each drop into the perch's context store and then **deletes** it. The
//! mind is read-only on drop files; it never deletes (6.4 — the removed-delete
//! fix). The write is precedence-guarded (6.5, [`crate::context`]) because a
//! dropped commune is an LLM snapshot.
//
// Clean-room over the M2a seams; the watch/poll cadence is the pulse (T5) — this
// module is the per-pass ingest mechanism it drives.
// [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER]
// [impl->REQ-API-3]

use crate::context::{write_context, WriteOutcome, WriteSource};
use spt_store::perch::{resolve_perch_path, ParentHint};
use std::path::{Path, PathBuf};

/// Fixed drop-file suffixes (spt-core owns the filenames; the adapter owns only
/// the directory — MANIFEST §watched dirs).
pub const COMMUNE_SUFFIX: &str = "-commune.md";
pub const SIGNOFF_SUFFIX: &str = "-signoff.md";
/// The per-perch context store file ingested drops fold into.
pub const CONTEXT_FILE: &str = "context.md";

/// Which kind of drop was ingested.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropKind {
    Commune,
    Signoff,
}

/// One ingested drop: its kind, the guarded-write outcome, and the (now deleted)
/// source path.
#[derive(Debug, Clone)]
pub struct Ingested {
    pub kind: DropKind,
    pub outcome: WriteOutcome,
    pub drop_path: PathBuf,
}

/// The perch's context store path.
pub fn context_path(id: &str, hint: ParentHint<'_>) -> PathBuf {
    resolve_perch_path(id, hint).join(CONTEXT_FILE)
}

/// Ingest any `<id>-commune.md` / `<id>-signoff.md` present in `drop_dir` for
/// `id`: fold each into the perch's context store (precedence-guarded, llm
/// source) and **delete** it (spt-core single-writer; the mind never deletes).
///
/// A drop is deleted whether its write `Written` or `Suppressed` — both mean
/// "consumed" (a suppressed drop was a stale snapshot, correctly discarded). A
/// read/write *error* leaves the drop in place to retry on the next pass.
pub fn ingest_drops(
    drop_dir: &Path,
    id: &str,
    hint: ParentHint<'_>,
    now_ms: u64,
    window_ms: u64,
) -> std::io::Result<Vec<Ingested>> {
    let ctx = context_path(id, hint);
    if let Some(parent) = ctx.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let mut out = Vec::new();
    for (kind, suffix) in [
        (DropKind::Commune, COMMUNE_SUFFIX),
        (DropKind::Signoff, SIGNOFF_SUFFIX),
    ] {
        let drop_path = drop_dir.join(format!("{id}{suffix}"));
        if !drop_path.exists() {
            continue;
        }
        let body = std::fs::read_to_string(&drop_path)?;
        // A dropped commune/signoff is an LLM-authored snapshot.
        let outcome = write_context(&ctx, &body, WriteSource::Llm, now_ms, window_ms)?;
        // [impl->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] spt-core deletes the drop
        // after consuming it; the mind never does.
        std::fs::remove_file(&drop_path)?;
        out.push(Ingested {
            kind,
            outcome,
            drop_path,
        });
    }
    Ok(out)
}

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

    #[test] // [unit->REQ-HAZARD-DROP-FILE-SINGLE-WRITER] a dropped commune is ingested + deleted by spt-core.
    fn commune_drop_is_ingested_and_deleted() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let drop = drops.path().join("doyle-commune.md");
            std::fs::write(&drop, "Source: echo-commune\nthe brief").unwrap();

            let ingested = ingest_drops(drops.path(), "doyle", ParentHint::Infer, 1000, 60_000)
                .expect("ingest");
            assert_eq!(ingested.len(), 1);
            assert_eq!(ingested[0].kind, DropKind::Commune);
            assert!(matches!(ingested[0].outcome, WriteOutcome::Written { .. }));

            // spt-core deleted the drop (single-writer ingest→delete).
            assert!(!drop.exists(), "drop must be deleted after ingest");
            // Content folded into the perch's context store.
            let ctx = context_path("doyle", ParentHint::Infer);
            assert!(std::fs::read_to_string(&ctx).unwrap().contains("the brief"));
        });
    }

    #[test] // [unit->REQ-API-3] commune + signoff drops both ingest in one pass.
    fn both_drops_ingested() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            std::fs::write(drops.path().join("doyle-commune.md"), "c").unwrap();
            std::fs::write(drops.path().join("doyle-signoff.md"), "s").unwrap();
            let ingested = ingest_drops(drops.path(), "doyle", ParentHint::Infer, 1000, 60_000)
                .expect("ingest");
            let kinds: Vec<DropKind> = ingested.iter().map(|i| i.kind).collect();
            assert!(kinds.contains(&DropKind::Commune) && kinds.contains(&DropKind::Signoff));
            assert!(!drops.path().join("doyle-commune.md").exists());
            assert!(!drops.path().join("doyle-signoff.md").exists());
        });
    }

    #[test] // [unit->REQ-HAZARD-DIRECT-WRITE-PRECEDENCE] a stale commune within a direct window is suppressed but still consumed.
    fn stale_commune_suppressed_but_deleted() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            // Seed a fresh direct write into the context store first.
            let ctx = context_path("doyle", ParentHint::Infer);
            std::fs::create_dir_all(ctx.parent().unwrap()).unwrap();
            write_context(&ctx, "DIRECT", WriteSource::Direct, 1000, DEFAULT_PROTECTION_WINDOW_MS)
                .unwrap();

            // A commune drop landing within the window must be suppressed.
            let drop = drops.path().join("doyle-commune.md");
            std::fs::write(&drop, "STALE COMMUNE").unwrap();
            let ingested =
                ingest_drops(drops.path(), "doyle", ParentHint::Infer, 1500, DEFAULT_PROTECTION_WINDOW_MS)
                    .expect("ingest");
            assert!(matches!(ingested[0].outcome, WriteOutcome::Suppressed { .. }));
            // Still consumed (deleted) and the direct write preserved.
            assert!(!drop.exists());
            let after = std::fs::read_to_string(&ctx).unwrap();
            assert!(after.contains("DIRECT") && !after.contains("STALE COMMUNE"));
        });
    }

    #[test] // no drops present ⇒ a clean empty pass, no error.
    fn no_drops_is_empty() {
        with_home(|_| {
            let drops = tempfile::tempdir().unwrap();
            let ingested =
                ingest_drops(drops.path(), "doyle", ParentHint::Infer, 1000, 60_000).expect("ingest");
            assert!(ingested.is_empty());
        });
    }
}


Prompt:
<task-notification>
<task-id>bwfbkkgia</task-id>
<tool-use-id>toolu_01MFY59gnmb811wnL2sLwmHi</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\bwfbkkgia.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI until 8b42452 completes" completed (exit code 0)</summary>
</task-notification>


1	[{"conclusion":"","headSha":"8b42452ce7f8277b239242efd853dc93e763fc17","status":"in_progress"}]
2	[{"conclusion":"success","headSha":"8b42452ce7f8277b239242efd853dc93e763fc17","status":"completed"}]
3

[{"conclusion":"","headSha":"8b42452ce7f8277b239242efd853dc93e763fc17","status":"in_progress"}]
[{"conclusion":"success","headSha":"8b42452ce7f8277b239242efd853dc93e763fc17","status":"completed"}]


D6a-1 CI **green** (`8b42452` success, final conclusion verified). Task 1 done. Continuing D6a-2 — signoff + resume:


Updated task #1 status


1	//! signoff + boundary lifecycle (M2b T6) — the load-bearing ordering invariants.
2	//!
3	//! These are the sister project's most expensive bugs; we **copy the ordering**
4	//! and clean-room the wiring (the concrete echo/compose/deliver effects are the
5	//! caller's — T8). Three invariants:
6	//!
7	//! - **3.3 echo-before-signoff** ([`graceful_signoff`]): synchronously run the
8	//!   echo-commune (final context delta) *before* composing INIT_SIGNOFF; skip
9	//!   the echo only when `session_id` is missing.
10	//! - **1.1 grace-before-signoff** ([`graceful_signoff`]): the grace wait
11	//!   completes and the recheck binds `still_gone` *before* any signoff write —
12	//!   a Self that transiently recovered (binary handoff, brief stale poll) is
13	//!   **not** torn down.
14	//! - **3.2 stale-signoff sweep** ([`sweep_stale_signoff`]): on every listener
15	//!   start, sweep a leftover `<id>-signoff.md` so a prior generation's sentinel
16	//!   never reads as a live signoff and kills a fresh start.
17	//!
18	//! Plus the **boundary Self-resume commune** ([`write_resume_commune`]): M2a's
19	//! `api boundary` rebinds the rotated session; M2b authors the resume commune
20	//! the re-entry (T7) preloads.
21	//
22	// M2b implements the GRACEFUL teardown paths (spt shutdown / spt suspend /
23	// boundary). Orphan *crash* detection after an unexpected harness death needs
24	// the always-on supervisor — that orphan-watch is M3, and it reuses this exact
25	// ordering. Copy the ordering, not the CC-coupled wiring.
26	// [impl->REQ-HAZARD-ECHO-BEFORE-SIGNOFF]
27	// [impl->REQ-HAZARD-GRACE-BEFORE-SIGNOFF]
28	// [impl->REQ-HAZARD-STALE-SIGNOFF-SENTINEL]
29
30	use crate::context::{write_context, WriteOutcome, WriteSource};
31	use crate::echo::stamp_provenance;
32	use crate::ingest::context_path;
33	use spt_store::perch::ParentHint;
34	use std::path::Path;
35	use std::time::Duration;
36
37	/// Provenance tag for a Self-authored boundary commune (strong live signal —
38	/// `docs/CONTEXT-MEMORY.md` commune-source asymmetry).
39	pub const SOURCE_SELF_COMMUNE: &str = "self-commune";
40
41	/// Outcome of a graceful signoff attempt.
42	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43	pub struct SignoffOutcome {
44	    /// Whether the echo-commune ran (false ⇒ skipped: no session_id).
45	    pub echo_ran: bool,

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

use crate::context::{write_context, WriteOutcome, WriteSource};
use crate::echo::stamp_provenance;
use crate::ingest::context_path;
use spt_store::perch::ParentHint;
use std::path::Path;
use std::time::Duration;

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

/// Outcome of a graceful signoff attempt.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SignoffOutcome {
    /// Whether the echo-commune ran (false ⇒ skipped: no session_id).
    pub echo_ran: bool,


Edit

use crate::context::{write_context, WriteOutcome, WriteSource};
use crate::echo::stamp_provenance;
use crate::ingest::context_path;
use spt_store::perch::ParentHint;
use std::path::Path;
use std::time::Duration;

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


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

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

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

use crate::context::{write_context, WriteOutcome, WriteSource};
use crate::echo::stamp_provenance;
use crate::ingest::context_path;
use spt_store::perch::ParentHint;
use std::path::Path;
use std::time::Duration;

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

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

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

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

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

    Ok(SignoffOutcome {
        echo_ran,
        signed_off,
    })
}

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

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

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

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

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

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

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

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

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

-use crate::context::{write_context, WriteOutcome, WriteSource};


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

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