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)

impl ContextStore {
    /// The context arm of `spt fork <src> <new>` (D9-5, REQ-INST-15 —
    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s live
    /// + project tiers, after which the two diverge — the copy lands as
    /// fresh seed commits on `a-<new>` / under `<new>/` in each project
    /// worktree (**no shared history, no ongoing sync**: a fork is a new
    /// identity, not an instance). Conflict artifacts are deliberately not
    /// copied — the fork starts clean. Refuses when `a-<new>` already exists
    /// (never merge two minds — the rename rule); a source with no tracked
    /// mind forks as an empty (perch-only) endpoint, `Ok`.
    // [impl->REQ-INST-15]
    pub fn fork_endpoint(&self, src: &str, new: &str) -> std::io::Result<()> {
        Self::validated(src)?;
        Self::validated(new)?;
        if self.store.has_branch(&agent_branch(new))? {
            return Err(std::io::Error::new(
                std::io::ErrorKind::AlreadyExists,
                format!("context branch already exists: {}", agent_branch(new)),
            ));
        }

        // Live tier: copy the tip content into a fresh, parentless a-<new>.
        if self.store.has_branch(&agent_branch(src))? {
            if let Some(bytes) =
                self.store.read_at_tip(&agent_branch(src), LIVE_CONTEXT_FILE)?
            {
                let dest = self.live_context_path(new)?;
                crate::atomic::atomic_write_string(&dest, &String::from_utf8_lossy(&bytes))?;
                self.commit_live(new, &format!("fork of {src} (one-time copy)"))?;
            }
        }

        // Project tiers: copy src's slice in 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();
                let src_file = wt.join(src).join(PROJECT_CONTEXT_FILE);
                if !wt.join(".git").exists() || !src_file.exists() {
                    continue;
                }
                let Some(project_id) =
                    entry.file_name().to_str().map(str::to_string)
                else {
                    continue;
                };
                let bytes = std::fs::read(&src_file)?;
                let dest = self.project_context_path(&project_id, new)?;
                crate::atomic::atomic_write_string(&dest, &String::from_utf8_lossy(&bytes))?;
                self.commit_project(&project_id, &format!("fork of {src} -> {new}"))?;
            }
        }
        Ok(())
    }

    /// Remove an endpoint's tracked mind — the `--delete-source` arm of
    /// `spt fork` (and nothing else: deletion is explicit, never implied).
    /// Removes the `a-<id>` worktree + branch and `<id>/` inside every
    /// project worktree (one removal commit per touched project). Absent
    /// pieces are no-ops — removing exactly what exists, idempotently.
    // [impl->REQ-INST-15]
    pub fn remove_endpoint(&self, id: &str) -> std::io::Result<()> {
        Self::validated(id)?;
        if self.store.has_branch(&agent_branch(id))? {
            let wt = self.root.join("agents").join(id);
            if wt.join(".git").exists() {
                crate::gitrun::run_git_ok(
                    &[
                        "-C",
                        &self.root.join(".seed.git").to_string_lossy(),
                        "worktree",
                        "remove",
                        "--force",
                        &wt.to_string_lossy(),
                    ],
                    None,
                    None,
                )?;
            }
            crate::gitrun::run_git_ok(
                &[
                    "-C",
                    &self.root.join(".seed.git").to_string_lossy(),
                    "branch",
                    "-D",
                    &agent_branch(id),
                ],
                None,
                None,
            )?;
        }
        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(id).exists() {
                    continue;
                }
                crate::gitrun::run_git_ok(&["rm", "-r", id], Some(&wt), None)?;
                self.store.commit_in_worktree(&wt, &[], &format!("remove {id} (fork source deleted)"))?;
            }
        }
        Ok(())
    }
}

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

+    /// The context arm of `spt fork <src> <new>` (D9-5, REQ-INST-15 —

+    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s live

+    /// + project tiers, after which the two diverge — the copy lands as

+    /// fresh seed commits on `a-<new>` / under `<new>/` in each project

+    /// worktree (**no shared history, no ongoing sync**: a fork is a new

+    /// identity, not an instance). Conflict artifacts are deliberately not

+    /// copied — the fork starts clean. Refuses when `a-<new>` already exists

+    /// (never merge two minds — the rename rule); a source with no tracked

+    /// mind forks as an empty (perch-only) endpoint, `Ok`.

+    pub fn fork_endpoint(&self, src: &str, new: &str) -> std::io::Result<()> {

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

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

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

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

+        // Live tier: copy the tip content into a fresh, parentless a-<new>.

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

+                self.store.read_at_tip(&agent_branch(src), LIVE_CONTEXT_FILE)?

+                let dest = self.live_context_path(new)?;

+                crate::atomic::atomic_write_string(&dest, &String::from_utf8_lossy(&bytes))?;

+                self.commit_live(new, &format!("fork of {src} (one-time copy)"))?;

+        // Project tiers: copy src's slice in 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 src_file = wt.join(src).join(PROJECT_CONTEXT_FILE);

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

+                    entry.file_name().to_str().map(str::to_string)

+                let bytes = std::fs::read(&src_file)?;

+                let dest = self.project_context_path(&project_id, new)?;

+                self.commit_project(&project_id, &format!("fork of {src} -> {new}"))?;

+    /// Remove an endpoint's tracked mind — the `--delete-source` arm of

+    /// `spt fork` (and nothing else: deletion is explicit, never implied).

+    /// Removes the `a-<id>` worktree + branch and `<id>/` inside every

+    /// project worktree (one removal commit per touched project). Absent

+    /// pieces are no-ops — removing exactly what exists, idempotently.

+    pub fn remove_endpoint(&self, id: &str) -> std::io::Result<()> {

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

+            let wt = self.root.join("agents").join(id);

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

+                crate::gitrun::run_git_ok(

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

+                        &wt.to_string_lossy(),

+            for entry in entries.flatten() {

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

+                crate::gitrun::run_git_ok(&["rm", "-r", id], Some(&wt), None)?;

+                self.store.commit_in_worktree(&wt, &[], &format!("remove {id} (fork source deleted)"))?;


Prompt:
<task-notification>
<task-id>bwlsid7km</task-id>
<tool-use-id>toolu_01KrxhUNqFZoYDiQd9oc5qCn</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\bwlsid7km.output</output-file>
<status>completed</status>
<summary>Background command "Wait for D9-5-1 CI run" completed (exit code 0)</summary>
</task-notification>


1	0ea318b completed success
2

0ea318b completed success


PowerShell

gh run list --branch dev-freeform --limit 2 --json status,conclusion,headSha | ConvertFrom-Json | Format-Table

Check D9-5-1 run status


conclusion headSha                                  status
---------- -------                                  ------
           e69f59add554232ee75321a10a1641ab2bbece91 in_progress
success    0ea318b3d77cdcc84c56fe45dad84de095baff96 completed


Bash

sha=e69f59add554232ee75321a10a1641ab2bbece91; until gh run list --branch dev-freeform --limit 3 --json status,conclusion,headSha --jq ".[] | select(.headSha==\"$sha\")" | grep -q '"status":"completed"'; do sleep 30; done; gh run list --branch dev-freeform --limit 3 --json status,conclusion,headSha --jq ".[] | select(.headSha==\"$sha\") | \"\(.headSha[0:7]) \(.status) \(.conclusion)\""

Wait for e69f59a run specifically


Command running in background with ID: b40rlg5w3. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b40rlg5w3.output. You will be notified when it completes. To check interim output, use Read on that file path.


Sha-pinned watcher armed. Contextstore fork tests:


Edit

#[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a surfaced conflict preserves BOTH versions; only a resolve clears it.

    // [unit->REQ-INST-15] fork copies-then-DIVERGES (ADR-0010): both tiers
    // land in the fork as one-time copies; a post-fork write on the SOURCE
    // never reaches the fork (no ongoing sync — a fork is not an instance);
    // an existing target mind refuses (never merge two minds).
    #[test]
    fn fork_copies_both_tiers_then_diverges() {
        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, "the mind at fork time").unwrap();
        cs.commit_live("doyle", "v1").unwrap().unwrap();
        let proj = cs.project_context_path("spt-core", "doyle").unwrap();
        std::fs::write(&proj, "project slice at fork time").unwrap();
        cs.commit_project("spt-core", "v1").unwrap().unwrap();

        cs.fork_endpoint("doyle", "doyle-work").unwrap();

        // Both tiers copied under the NEW identity; the source is untouched.
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&agent_branch("doyle-work"), LIVE_CONTEXT_FILE)
                .unwrap(),
            Some(b"the mind at fork time".to_vec())
        );
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&project_branch("spt-core"), "doyle-work/project-context.md")
                .unwrap(),
            Some(b"project slice at fork time".to_vec())
        );
        assert_eq!(
            cs.branch_store().read_at_tip(&agent_branch("doyle"), LIVE_CONTEXT_FILE).unwrap(),
            Some(b"the mind at fork time".to_vec()),
            "source untouched"
        );

        // DIVERGENCE: a post-fork source write never reaches the fork.
        std::fs::write(&live, "the mind moved on").unwrap();
        cs.commit_live("doyle", "v2").unwrap().unwrap();
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&agent_branch("doyle-work"), LIVE_CONTEXT_FILE)
                .unwrap(),
            Some(b"the mind at fork time".to_vec()),
            "copied-then-independent: no ongoing sync"
        );

        // Collision: an existing target mind refuses.
        let err = cs.fork_endpoint("doyle", "doyle-work").unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);

        // A perch-only (untracked) source forks as a clean no-op.
        cs.fork_endpoint("ghost", "ghost-copy").unwrap();
        assert!(!cs.branch_store().has_branch(&agent_branch("ghost-copy")).unwrap());
    }

    // [unit->REQ-INST-15] delete-source removes EXACTLY the source: its a-
    // branch + worktree and its project slices go; the fork's mind and other
    // agents' slices stay. Idempotent on an already-removed id.
    #[test]
    fn remove_endpoint_removes_exactly_the_source() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        for id in ["doyle", "momo"] {
            let live = cs.live_context_path(id).unwrap();
            std::fs::write(&live, format!("{id} mind")).unwrap();
            cs.commit_live(id, "v1").unwrap().unwrap();
            let proj = cs.project_context_path("spt-core", id).unwrap();
            std::fs::write(&proj, format!("{id} slice")).unwrap();
        }
        cs.commit_project("spt-core", "both").unwrap().unwrap();
        cs.fork_endpoint("doyle", "doyle-work").unwrap();

        cs.remove_endpoint("doyle").unwrap();

        let bs = cs.branch_store();
        assert!(!bs.has_branch(&agent_branch("doyle")).unwrap(), "source mind gone");
        assert!(!tmp.path().join("agents").join("doyle").exists(), "worktree gone");
        assert!(
            bs.read_at_tip(&project_branch("spt-core"), "doyle/project-context.md")
                .unwrap()
                .is_none(),
            "source project slice gone"
        );
        // Exactly the source: the fork and the other agent survive.
        assert_eq!(
            bs.read_at_tip(&agent_branch("doyle-work"), LIVE_CONTEXT_FILE).unwrap(),
            Some(b"doyle mind".to_vec())
        );
        assert_eq!(
            bs.read_at_tip(&project_branch("spt-core"), "momo/project-context.md").unwrap(),
            Some(b"momo slice".to_vec())
        );

        // Idempotent: removing again is a no-op.
        cs.remove_endpoint("doyle").unwrap();
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a surfaced conflict preserves BOTH versions; only a resolve clears it.


    // [unit->REQ-INST-15] fork copies-then-DIVERGES (ADR-0010): both tiers
    // land in the fork as one-time copies; a post-fork write on the SOURCE
    // never reaches the fork (no ongoing sync — a fork is not an instance);
    // an existing target mind refuses (never merge two minds).
    #[test]
    fn fork_copies_both_tiers_then_diverges() {
        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, "the mind at fork time").unwrap();
        cs.commit_live("doyle", "v1").unwrap().unwrap();
        let proj = cs.project_context_path("spt-core", "doyle").unwrap();
        std::fs::write(&proj, "project slice at fork time").unwrap();
        cs.commit_project("spt-core", "v1").unwrap().unwrap();

        cs.fork_endpoint("doyle", "doyle-work").unwrap();

        // Both tiers copied under the NEW identity; the source is untouched.
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&agent_branch("doyle-work"), LIVE_CONTEXT_FILE)
                .unwrap(),
            Some(b"the mind at fork time".to_vec())
        );
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&project_branch("spt-core"), "doyle-work/project-context.md")
                .unwrap(),
            Some(b"project slice at fork time".to_vec())
        );
        assert_eq!(
            cs.branch_store().read_at_tip(&agent_branch("doyle"), LIVE_CONTEXT_FILE).unwrap(),
            Some(b"the mind at fork time".to_vec()),
            "source untouched"
        );

        // DIVERGENCE: a post-fork source write never reaches the fork.
        std::fs::write(&live, "the mind moved on").unwrap();
        cs.commit_live("doyle", "v2").unwrap().unwrap();
        assert_eq!(
            cs.branch_store()
                .read_at_tip(&agent_branch("doyle-work"), LIVE_CONTEXT_FILE)
                .unwrap(),
            Some(b"the mind at fork time".to_vec()),
            "copied-then-independent: no ongoing sync"
        );

        // Collision: an existing target mind refuses.
        let err = cs.fork_endpoint("doyle", "doyle-work").unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);

        // A perch-only (untracked) source forks as a clean no-op.
        cs.fork_endpoint("ghost", "ghost-copy").unwrap();
        assert!(!cs.branch_store().has_branch(&agent_branch("ghost-copy")).unwrap());
    }

    // [unit->REQ-INST-15] delete-source removes EXACTLY the source: its a-
    // branch + worktree and its project slices go; the fork's mind and other
    // agents' slices stay. Idempotent on an already-removed id.
    #[test]
    fn remove_endpoint_removes_exactly_the_source() {
        let tmp = tempdir().unwrap();
        let cs = ContextStore::open_or_init_in(tmp.path()).unwrap();
        for id in ["doyle", "momo"] {
            let live = cs.live_context_path(id).unwrap();
            std::fs::write(&live, format!("{id} mind")).unwrap();
            cs.commit_live(id, "v1").unwrap().unwrap();
            let proj = cs.project_context_path("spt-core", id).unwrap();
            std::fs::write(&proj, format!("{id} slice")).unwrap();
        }
        cs.commit_project("spt-core", "both").unwrap().unwrap();
        cs.fork_endpoint("doyle", "doyle-work").unwrap();

        cs.remove_endpoint("doyle").unwrap();

        let bs = cs.branch_store();
        assert!(!bs.has_branch(&agent_branch("doyle")).unwrap(), "source mind gone");
        assert!(!tmp.path().join("agents").join("doyle").exists(), "worktree gone");
        assert!(
            bs.read_at_tip(&project_branch("spt-core"), "doyle/project-context.md")
                .unwrap()
                .is_none(),
            "source project slice gone"
        );
        // Exactly the source: the fork and the other agent survive.
        assert_eq!(
            bs.read_at_tip(&agent_branch("doyle-work"), LIVE_CONTEXT_FILE).unwrap(),
            Some(b"doyle mind".to_vec())
        );
        assert_eq!(
            bs.read_at_tip(&project_branch("spt-core"), "momo/project-context.md").unwrap(),
            Some(b"momo slice".to_vec())
        );

        // Idempotent: removing again is a no-op.
        cs.remove_endpoint("doyle").unwrap();
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] a surfaced conflict preserves BOTH versions; only a resolve clears it.

+    // [unit->REQ-INST-15] fork copies-then-DIVERGES (ADR-0010): both tiers

+    // land in the fork as one-time copies; a post-fork write on the SOURCE

+    // never reaches the fork (no ongoing sync — a fork is not an instance);

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

+    fn fork_copies_both_tiers_then_diverges() {

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

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

+        std::fs::write(&live, "the mind at fork time").unwrap();

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

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

+        std::fs::write(&proj, "project slice at fork time").unwrap();

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

+        cs.fork_endpoint("doyle", "doyle-work").unwrap();

+        // Both tiers copied under the NEW identity; the source is untouched.

+                .read_at_tip(&agent_branch("doyle-work"), LIVE_CONTEXT_FILE)

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

+            Some(b"project slice at fork time".to_vec())

+            cs.branch_store().read_at_tip(&agent_branch("doyle"), LIVE_CONTEXT_FILE).unwrap(),

+            Some(b"the mind at fork time".to_vec()),

+        // DIVERGENCE: a post-fork source write never reaches the fork.

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

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

+            "copied-then-independent: no ongoing sync"

+        // Collision: an existing target mind refuses.

+        let err = cs.fork_endpoint("doyle", "doyle-work").unwrap_err();

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

+        // A perch-only (untracked) source forks as a clean no-op.

+        cs.fork_endpoint("ghost", "ghost-copy").unwrap();

+        assert!(!cs.branch_store().has_branch(&agent_branch("ghost-copy")).unwrap());

+    // [unit->REQ-INST-15] delete-source removes EXACTLY the source: its a-

+    // branch + worktree and its project slices go; the fork's mind and other

+    // agents' slices stay. Idempotent on an already-removed id.

+    fn remove_endpoint_removes_exactly_the_source() {

+            let live = cs.live_context_path(id).unwrap();

+            std::fs::write(&live, format!("{id} mind")).unwrap();

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

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

+            std::fs::write(&proj, format!("{id} slice")).unwrap();

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

+        cs.remove_endpoint("doyle").unwrap();

+        assert!(!bs.has_branch(&agent_branch("doyle")).unwrap(), "source mind gone");

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

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

+            "source project slice gone"

+        // Exactly the source: the fork and the other agent survive.

+            bs.read_at_tip(&agent_branch("doyle-work"), LIVE_CONTEXT_FILE).unwrap(),

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

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

+            Some(b"momo slice".to_vec())

+        // Idempotent: removing again is a no-op.


D9-3: dormancy budget measurement + DORMANCY-BUDGET.md + ADR-0003 #9 close

SPT_BUDGET=1 harness (done, uncommitted). HFENDULEAM runs done (shell 7.7MiB, claude 308MiB, zero idle CPU). Remaining: gravity-linux leg (workflow_dispatch budget job on gravity runner), docs/DORMANCY-BUDGET.md (methodology + numbers + policy lock, doc->REQ-INST-3), ADR-0003 #9 amendment, CONTEXT.md policy confirm. CI green.

D9-4: REQ-INST-14 resource advertisement (blurb on Instance + spt resources CLI + projection)

resources: Option<String> on Instance (serde-default, rides RegistryUpdate + lease); resource_projection over visible+routable rows; info.json resources field + set_resources; daemon.json resources_blurb node seed (drop Copy on DaemonConfig); advertise_local fills endpoint-else-node blurb; spt resources set/show/list CLI; CONTEXT doc tag; activate REQ-INST-14 [doc, impl, unit]. CI green.

D9-5: REQ-INST-15 immutable home subnet + spt fork

D9-5-1: home model (info.json home_subnet+adapter, assign_home matrix, creation-seam stamping + carry-forward on revive, sync_subnets=[home] seed, first-join adoption via CLI create + advertise sweep). D9-5-2: spt fork (ContextStore::fork_endpoint seed-commit copy both tiers, join-time collision vs target snapshot, new perch home=target, --delete-source). ADR-0010 doc tags; activate REQ-INST-15 [doc, impl, unit].


PowerShell

cargo test -p spt-store contextstore 2>&1 | Select-String -Pattern "test result:|error|panicked|FAILED" | Select-Object -First 8

Run contextstore tests


test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 123 filtered out; finished in 2.40s


Store layer green. CLI fork command:


Edit

/// The subnet resource registry (REQ-INST-14): per-endpoint free-text

    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,
    /// ADR-0010: home is immutable — fork is the cross-subnet move, never a
    /// re-home). Seeds the fork with a one-time copy of the source's mind
    /// (live + project tiers); the two diverge immediately (no ongoing
    /// sync). The source is untouched unless --delete-source. Same-node only
    /// in v1 — the remote arm composes with M5 instantiate-anywhere's
    /// consent gate, and the same-name default rides it (one node holds one
    /// perch per name, so a local fork needs a new id).
    Fork {
        /// The source endpoint (must exist on this node).
        src: String,
        /// The fork's id (must differ from the source on the same node).
        new_id: String,
        /// The fork's home subnet — the target (must be a member).
        #[arg(long)]
        subnet: String,
        /// Delete the source endpoint (perch + tracked mind) after the copy.
        #[arg(long)]
        delete_source: bool,
    },
    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text


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

    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,
    /// ADR-0010: home is immutable — fork is the cross-subnet move, never a
    /// re-home). Seeds the fork with a one-time copy of the source's mind
    /// (live + project tiers); the two diverge immediately (no ongoing
    /// sync). The source is untouched unless --delete-source. Same-node only
    /// in v1 — the remote arm composes with M5 instantiate-anywhere's
    /// consent gate, and the same-name default rides it (one node holds one
    /// perch per name, so a local fork needs a new id).
    Fork {
        /// The source endpoint (must exist on this node).
        src: String,
        /// The fork's id (must differ from the source on the same node).
        new_id: String,
        /// The fork's home subnet — the target (must be a member).
        #[arg(long)]
        subnet: String,
        /// Delete the source endpoint (perch + tracked mind) after the copy.
        #[arg(long)]
        delete_source: bool,
    },
    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text

+    /// Fork an endpoint into another subnet as a NEW identity (REQ-INST-15,

+    /// ADR-0010: home is immutable — fork is the cross-subnet move, never a

+    /// re-home). Seeds the fork with a one-time copy of the source's mind

+    /// (live + project tiers); the two diverge immediately (no ongoing

+    /// sync). The source is untouched unless --delete-source. Same-node only

+    /// in v1 — the remote arm composes with M5 instantiate-anywhere's

+    /// consent gate, and the same-name default rides it (one node holds one

+    /// perch per name, so a local fork needs a new id).

+        /// The source endpoint (must exist on this node).

+        /// The fork's id (must differ from the source on the same node).

+        /// The fork's home subnet — the target (must be a member).

+        /// Delete the source endpoint (perch + tracked mind) after the copy.

/// service blurbs — an agent yellow-pages. `set` authors your own,

/// `show` reads a local endpoint's, `list` projects the registry view


Cmd::Resources { action } => cmd_resources(action),

        Cmd::Fork { src, new_id, subnet, delete_source } => {
            cmd_fork(&src, &new_id, &subnet, delete_source)
        }
        Cmd::Resources { action } => cmd_resources(action),


        Cmd::Fork { src, new_id, subnet, delete_source } => {
            cmd_fork(&src, &new_id, &subnet, delete_source)
        }
        Cmd::Resources { action } => cmd_resources(action),

NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),

Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),

+        Cmd::Fork { src, new_id, subnet, delete_source } => {

+            cmd_fork(&src, &new_id, &subnet, delete_source)

Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),

Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),


/// `spt resources set|show|list` — the subnet resource registry surface

/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move
/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
/// runs before anything mutates, so a refused fork leaves no half-fork.
///
/// 1. charset + same-id guard (a node holds one perch per name — the
///    same-name fork is the M5 remote arm);
/// 2. target membership (the fork's home must be a subnet we hold);
/// 3. source exists locally (the remote-source arm is M5);
/// 4. join-time collision in the TARGET subnet (the snapshot's rows — any
///    node, any status: a resting holder still owns its name) and locally;
/// 5. context fork (one-time copy, copied-then-independent) + perch mint
///    (home = target, adapter/blurb carried — the fork starts as the same
///    kind of agent) + sync scope seeded [target];
/// 6. `--delete-source` LAST: the source vanishes only after the fork is
///    whole (perch + tracked mind + address/ready marker).
// [impl->REQ-INST-15]
fn cmd_fork(src: &str, new_id: &str, subnet: &str, delete_source: bool) -> i32 {
    use spt_store::contextstore::ContextStore;

    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {
        eprintln!("ERROR: invalid new id: {e}");
        return 2;
    }
    if src == new_id {
        eprintln!(
            "SAME_ID:{src} — a same-node fork needs a new id (one perch per name per node); \
             the same-name fork rides the M5 remote arm"
        );
        return 1;
    }
    let subnets = SubnetStore::load();
    if subnets.find(subnet).is_none() {
        eprintln!("NO_SUBNET:{subnet} — this node is not a member; the fork's home must be held");
        return 1;
    }
    let src_perch = perch::resolve_perch_path(src, ParentHint::Infer);
    let Some(src_rec) = spt_store::info::read_info(&src_perch) else {
        eprintln!("NO_SOURCE:{src} — not on this node (the cross-node fork arm is M5)");
        return 1;
    };
    let new_perch = perch::resolve_perch_path(new_id, ParentHint::Infer);
    if spt_store::info::read_info(&new_perch).is_some() {
        eprintln!("TAKEN:{new_id} — a perch already holds this id on this node");
        return 1;
    }
    // Join-time collision in the target (ADR-0006 §1 via the stale-tolerant
    // snapshot): ANY holder — any node, any status — owns the name.
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    if let Some(reg) = regs.get(subnet) {
        let holders = reg.instances(new_id);
        if !holders.is_empty() {
            let nodes: Vec<&str> = holders.iter().map(|i| i.node.as_str()).collect();
            eprintln!(
                "COLLISION:{new_id} already exists in subnet '{subnet}' on node(s): {} — \
                 pick another id (or rename the holder first)",
                nodes.join(", ")
            );
            return 1;
        }
    }

    // The one-time mind copy (copied-then-independent, ADR-0010).
    let cs = match ContextStore::open_or_init() {
        Ok(cs) => cs,
        Err(e) => {
            eprintln!("CONTEXT_FAIL: {e}");
            return 1;
        }
    };
    if let Err(e) = cs.fork_endpoint(src, new_id) {
        eprintln!("FORK_FAIL:{src} -> {new_id}: {e}");
        return 1;
    }

    // Mint the fork's perch: a NEW identity homed in the target; the adapter
    // and resource blurb carry over (the fork starts as the same kind of
    // agent — the agent refines from there).
    if let Err(e) = std::fs::create_dir_all(&new_perch) {
        eprintln!("FORK_FAIL:{new_id}: {e}");
        return 1;
    }
    let mut rec = spt_store::info::InfoJson::new(
        new_id,
        &now_unix().to_string(),
        std::process::id(),
        "",
        "ready_agent",
    );
    rec.home_subnet = Some(subnet.to_string());
    rec.adapter = src_rec.adapter.clone();
    rec.resources = src_rec.resources.clone();
    if let Err(e) = spt_store::info::write_info(&new_perch, &rec) {
        eprintln!("FORK_FAIL:{new_id}: {e}");
        return 1;
    }
    let mut vis = spt_store::visibility::VisibilityStore::load();
    spt_store::home::seed_endpoint_scopes(&mut vis, new_id, subnet);
    let _ = vis.save();

    // --delete-source last: only after the fork is whole.
    if delete_source {
        if let Err(e) = cs.remove_endpoint(src) {
            eprintln!("DELETE_SOURCE_FAIL:{src}: {e} (the fork is complete; re-run to delete)");
            return 1;
        }
        let _ = std::fs::remove_file(perch::resolve_ready_file(src, ParentHint::Infer));
        let _ = registry::unregister_address(src, &perch::owlery_dir());
        if let Err(e) = std::fs::remove_dir_all(&src_perch) {
            eprintln!("DELETE_SOURCE_FAIL:{src}: {e} (the fork is complete; re-run to delete)");
            return 1;
        }
        eprintln!("SOURCE_DELETED:{src}");
    }
    eprintln!("FORKED:{src} -> {new_id} (home={subnet}, copied-then-independent)");
    0
}

/// `spt resources set|show|list` — the subnet resource registry surface


/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move
/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
/// runs before anything mutates, so a refused fork leaves no half-fork.
///
/// 1. charset + same-id guard (a node holds one perch per name — the
///    same-name fork is the M5 remote arm);
/// 2. target membership (the fork's home must be a subnet we hold);
/// 3. source exists locally (the remote-source arm is M5);
/// 4. join-time collision in the TARGET subnet (the snapshot's rows — any
///    node, any status: a resting holder still owns its name) and locally;
/// 5. context fork (one-time copy, copied-then-independent) + perch mint
///    (home = target, adapter/blurb carried — the fork starts as the same
///    kind of agent) + sync scope seeded [target];
/// 6. `--delete-source` LAST: the source vanishes only after the fork is
///    whole (perch + tracked mind + address/ready marker).
// [impl->REQ-INST-15]
fn cmd_fork(src: &str, new_id: &str, subnet: &str, delete_source: bool) -> i32 {
    use spt_store::contextstore::ContextStore;

    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {
        eprintln!("ERROR: invalid new id: {e}");
        return 2;
    }
    if src == new_id {
        eprintln!(
            "SAME_ID:{src} — a same-node fork needs a new id (one perch per name per node); \
             the same-name fork rides the M5 remote arm"
        );
        return 1;
    }
    let subnets = SubnetStore::load();
    if subnets.find(subnet).is_none() {
        eprintln!("NO_SUBNET:{subnet} — this node is not a member; the fork's home must be held");
        return 1;
    }
    let src_perch = perch::resolve_perch_path(src, ParentHint::Infer);
    let Some(src_rec) = spt_store::info::read_info(&src_perch) else {
        eprintln!("NO_SOURCE:{src} — not on this node (the cross-node fork arm is M5)");
        return 1;
    };
    let new_perch = perch::resolve_perch_path(new_id, ParentHint::Infer);
    if spt_store::info::read_info(&new_perch).is_some() {
        eprintln!("TAKEN:{new_id} — a perch already holds this id on this node");
        return 1;
    }
    // Join-time collision in the target (ADR-0006 §1 via the stale-tolerant
    // snapshot): ANY holder — any node, any status — owns the name.
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    if let Some(reg) = regs.get(subnet) {
        let holders = reg.instances(new_id);
        if !holders.is_empty() {
            let nodes: Vec<&str> = holders.iter().map(|i| i.node.as_str()).collect();
            eprintln!(
                "COLLISION:{new_id} already exists in subnet '{subnet}' on node(s): {} — \
                 pick another id (or rename the holder first)",
                nodes.join(", ")
            );
            return 1;
        }
    }

    // The one-time mind copy (copied-then-independent, ADR-0010).
    let cs = match ContextStore::open_or_init() {
        Ok(cs) => cs,
        Err(e) => {
            eprintln!("CONTEXT_FAIL: {e}");
            return 1;
        }
    };
    if let Err(e) = cs.fork_endpoint(src, new_id) {
        eprintln!("FORK_FAIL:{src} -> {new_id}: {e}");
        return 1;
    }

    // Mint the fork's perch: a NEW identity homed in the target; the adapter
    // and resource blurb carry over (the fork starts as the same kind of
    // agent — the agent refines from there).
    if let Err(e) = std::fs::create_dir_all(&new_perch) {
        eprintln!("FORK_FAIL:{new_id}: {e}");
        return 1;
    }
    let mut rec = spt_store::info::InfoJson::new(
        new_id,
        &now_unix().to_string(),
        std::process::id(),
        "",
        "ready_agent",
    );
    rec.home_subnet = Some(subnet.to_string());
    rec.adapter = src_rec.adapter.clone();
    rec.resources = src_rec.resources.clone();
    if let Err(e) = spt_store::info::write_info(&new_perch, &rec) {
        eprintln!("FORK_FAIL:{new_id}: {e}");
        return 1;
    }
    let mut vis = spt_store::visibility::VisibilityStore::load();
    spt_store::home::seed_endpoint_scopes(&mut vis, new_id, subnet);
    let _ = vis.save();

    // --delete-source last: only after the fork is whole.
    if delete_source {
        if let Err(e) = cs.remove_endpoint(src) {
            eprintln!("DELETE_SOURCE_FAIL:{src}: {e} (the fork is complete; re-run to delete)");
            return 1;
        }
        let _ = std::fs::remove_file(perch::resolve_ready_file(src, ParentHint::Infer));
        let _ = registry::unregister_address(src, &perch::owlery_dir());
        if let Err(e) = std::fs::remove_dir_all(&src_perch) {
            eprintln!("DELETE_SOURCE_FAIL:{src}: {e} (the fork is complete; re-run to delete)");
            return 1;
        }
        eprintln!("SOURCE_DELETED:{src}");
    }
    eprintln!("FORKED:{src} -> {new_id} (home={subnet}, copied-then-independent)");
    0
}

/// `spt resources set|show|list` — the subnet resource registry surface

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

+/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate

+/// runs before anything mutates, so a refused fork leaves no half-fork.

+/// 1. charset + same-id guard (a node holds one perch per name — the

+///    same-name fork is the M5 remote arm);

+/// 2. target membership (the fork's home must be a subnet we hold);

+/// 3. source exists locally (the remote-source arm is M5);

+/// 4. join-time collision in the TARGET subnet (the snapshot's rows — any

+///    node, any status: a resting holder still owns its name) and locally;

+/// 5. context fork (one-time copy, copied-then-independent) + perch mint

+///    (home = target, adapter/blurb carried — the fork starts as the same

+///    kind of agent) + sync scope seeded [target];

+/// 6. `--delete-source` LAST: the source vanishes only after the fork is

+///    whole (perch + tracked mind + address/ready marker).

+fn cmd_fork(src: &str, new_id: &str, subnet: &str, delete_source: bool) -> i32 {

+    use spt_store::contextstore::ContextStore;

+    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {

+        eprintln!("ERROR: invalid new id: {e}");

+            "SAME_ID:{src} — a same-node fork needs a new id (one perch per name per node); \

+             the same-name fork rides the M5 remote arm"

+    if subnets.find(subnet).is_none() {

+        eprintln!("NO_SUBNET:{subnet} — this node is not a member; the fork's home must be held");

+    let src_perch = perch::resolve_perch_path(src, ParentHint::Infer);

+    let Some(src_rec) = spt_store::info::read_info(&src_perch) else {

+        eprintln!("NO_SOURCE:{src} — not on this node (the cross-node fork arm is M5)");

+    let new_perch = perch::resolve_perch_path(new_id, ParentHint::Infer);

+    if spt_store::info::read_info(&new_perch).is_some() {

+        eprintln!("TAKEN:{new_id} — a perch already holds this id on this node");

+    // Join-time collision in the target (ADR-0006 §1 via the stale-tolerant

+    // snapshot): ANY holder — any node, any status — owns the name.

+    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));

+    if let Some(reg) = regs.get(subnet) {

+        let holders = reg.instances(new_id);

+            let nodes: Vec<&str> = holders.iter().map(|i| i.node.as_str()).collect();

+                "COLLISION:{new_id} already exists in subnet '{subnet}' on node(s): {} — \

+                 pick another id (or rename the holder first)",

+    // The one-time mind copy (copied-then-independent, ADR-0010).

+    let cs = match ContextStore::open_or_init() {

+            eprintln!("CONTEXT_FAIL: {e}");

+    if let Err(e) = cs.fork_endpoint(src, new_id) {

+        eprintln!("FORK_FAIL:{src} -> {new_id}: {e}");

+    // Mint the fork's perch: a NEW identity homed in the target; the adapter

+    // and resource blurb carry over (the fork starts as the same kind of

+    // agent — the agent refines from there).

+    if let Err(e) = std::fs::create_dir_all(&new_perch) {

+        eprintln!("FORK_FAIL:{new_id}: {e}");

+    let mut rec = spt_store::info::InfoJson::new(

+    rec.home_subnet = Some(subnet.to_string());

+    rec.adapter = src_rec.adapter.clone();

+    rec.resources = src_rec.resources.clone();

+    if let Err(e) = spt_store::info::write_info(&new_perch, &rec) {

+    let mut vis = spt_store::visibility::VisibilityStore::load();

+    spt_store::home::seed_endpoint_scopes(&mut vis, new_id, subnet);

+    // --delete-source last: only after the fork is whole.

+        if let Err(e) = cs.remove_endpoint(src) {

+            eprintln!("DELETE_SOURCE_FAIL:{src}: {e} (the fork is complete; re-run to delete)");

+        let _ = std::fs::remove_file(perch::resolve_ready_file(src, ParentHint::Infer));

+        let _ = registry::unregister_address(src, &perch::owlery_dir());

+        if let Err(e) = std::fs::remove_dir_all(&src_perch) {

+        eprintln!("SOURCE_DELETED:{src}");

+    eprintln!("FORKED:{src} -> {new_id} (home={subnet}, copied-then-independent)");

/// (D9-4, REQ-INST-14). `set`/`show` operate the LOCAL endpoint's authored

/// blurb on its perch record; `list` is the registry **projection** read off


CLI tests for fork:


// [unit->REQ-INST-14] the resources surfaces parse.

        // [unit->REQ-INST-15] the fork surface parses; the target is required.
        assert!(matches!(
            parse(&["spt", "fork", "ling", "ling-work", "--subnet", "work"]).unwrap().cmd,
            Cmd::Fork { delete_source: false, .. }
        ));
        assert!(matches!(
            parse(&["spt", "fork", "ling", "l2", "--subnet", "work", "--delete-source"])
                .unwrap()
                .cmd,
            Cmd::Fork { delete_source: true, .. }
        ));
        assert!(parse(&["spt", "fork", "ling", "l2"]).is_err(), "--subnet required");
        assert!(parse(&["spt", "fork", "ling"]).is_err(), "new id required");
        // [unit->REQ-INST-14] the resources surfaces parse.


        // [unit->REQ-INST-15] the fork surface parses; the target is required.
        assert!(matches!(
            parse(&["spt", "fork", "ling", "ling-work", "--subnet", "work"]).unwrap().cmd,
            Cmd::Fork { delete_source: false, .. }
        ));
        assert!(matches!(
            parse(&["spt", "fork", "ling", "l2", "--subnet", "work", "--delete-source"])
                .unwrap()
                .cmd,
            Cmd::Fork { delete_source: true, .. }
        ));
        assert!(parse(&["spt", "fork", "ling", "l2"]).is_err(), "--subnet required");
        assert!(parse(&["spt", "fork", "ling"]).is_err(), "new id required");
        // [unit->REQ-INST-14] the resources surfaces parse.

// [unit->REQ-INST-11] the rename surface parses (two positional ids).

assert!(matches!(parse(&["spt", "rename", "ling", "oak"]).unwrap().cmd, Cmd::Rename { .. }));

assert!(parse(&["spt", "rename", "ling"]).is_err(), "new id required");

+        // [unit->REQ-INST-15] the fork surface parses; the target is required.

+            parse(&["spt", "fork", "ling", "ling-work", "--subnet", "work"]).unwrap().cmd,

+            Cmd::Fork { delete_source: false, .. }

+            parse(&["spt", "fork", "ling", "l2", "--subnet", "work", "--delete-source"])

+            Cmd::Fork { delete_source: true, .. }

+        assert!(parse(&["spt", "fork", "ling", "l2"]).is_err(), "--subnet required");

+        assert!(parse(&["spt", "fork", "ling"]).is_err(), "new id required");

parse(&["spt", "resources", "set", "rust builds", "--id", "ling"]).unwrap().cmd,


// ── pair show-totp ─────────────────────────────────────────────────────────

    // [unit->REQ-INST-15] the fork command end to end on one node: copies
    // the mind + mints the perch homed in the TARGET (adapter carried, sync
    // scope seeded), refuses every collision class before mutating anything,
    // and --delete-source removes exactly the source.
    #[test]
    fn fork_forks_homes_and_deletes_exactly_the_source() {
        use spt_store::contextstore::{agent_branch, ContextStore, LIVE_CONTEXT_FILE};

        let _h = crate::testutil::isolated_home();
        let mut subnets = SubnetStore::load();
        subnets.create_subnet("home").unwrap();
        subnets.create_subnet("work").unwrap();
        subnets.save().unwrap();

        let src_perch = perch::resolve_perch_path("ling", ParentHint::Infer);
        std::fs::create_dir_all(&src_perch).unwrap();
        let mut rec = spt_store::info::InfoJson::new("ling", "t", 4242, "sid", "live_agent");
        rec.adapter = Some("mock".into());
        spt_store::info::write_info(&src_perch, &rec).unwrap();
        let cs = ContextStore::open_or_init().unwrap();
        let live = cs.live_context_path("ling").unwrap();
        std::fs::write(&live, "mind").unwrap();
        cs.commit_live("ling", "v1").unwrap();

        assert_eq!(cmd_fork("ling", "ling-work", "work", false), 0);
        let new_perch = perch::resolve_perch_path("ling-work", ParentHint::Infer);
        let new_rec = spt_store::info::read_info(&new_perch).unwrap();
        assert_eq!(new_rec.home_subnet.as_deref(), Some("work"), "the fork's home = target");
        assert_eq!(new_rec.adapter.as_deref(), Some("mock"), "adapter carries");
        assert_eq!(
            cs.branch_store().read_at_tip(&agent_branch("ling-work"), LIVE_CONTEXT_FILE).unwrap(),
            Some(b"mind".to_vec()),
            "one-time mind copy landed"
        );
        let vis = spt_store::visibility::VisibilityStore::load();
        assert_eq!(vis.sync_subnets("ling-work"), Some(&["work".to_string()][..]));

        // Refusal classes (each before any mutation).
        assert_eq!(cmd_fork("ling", "ling", "work", false), 1, "same id on one node");
        assert_eq!(cmd_fork("ling", "ling-work", "work", false), 1, "locally taken");
        assert_eq!(cmd_fork("ling", "other", "ghost", false), 1, "non-member target");
        assert_eq!(cmd_fork("ghost", "other", "work", false), 1, "missing source (M5 arm)");
        // Target-registry collision: ANY holder (even a resting one) owns it.
        let snap_dir = perch::identity_dir().join("registry");
        std::fs::create_dir_all(&snap_dir).unwrap();
        let mut reg = spt_net::net::registry::SubnetRegistry::new();
        reg.merge_instance(
            "claimed",
            spt_net::net::registry::Instance {
                node: "bb22".into(),
                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
            },
        );
        std::fs::write(snap_dir.join("work.json"), serde_json::to_string(&reg).unwrap())
            .unwrap();
        assert_eq!(cmd_fork("ling", "claimed", "work", false), 1, "target holder owns the name");

        // delete-source removes exactly the source.
        assert_eq!(cmd_fork("ling", "ling-cold", "work", true), 0);
        assert!(spt_store::info::read_info(&src_perch).is_none(), "source perch gone");
        assert!(
            !cs.branch_store().has_branch(&agent_branch("ling")).unwrap(),
            "source mind gone"
        );
        assert!(spt_store::info::read_info(&new_perch).is_some(), "earlier fork untouched");
        assert!(
            cs.branch_store().has_branch(&agent_branch("ling-cold")).unwrap(),
            "the deleting fork's own mind stands"
        );
    }

    // ── pair show-totp ─────────────────────────────────────────────────────────


    // [unit->REQ-INST-15] the fork command end to end on one node: copies
    // the mind + mints the perch homed in the TARGET (adapter carried, sync
    // scope seeded), refuses every collision class before mutating anything,
    // and --delete-source removes exactly the source.
    #[test]
    fn fork_forks_homes_and_deletes_exactly_the_source() {
        use spt_store::contextstore::{agent_branch, ContextStore, LIVE_CONTEXT_FILE};

        let _h = crate::testutil::isolated_home();
        let mut subnets = SubnetStore::load();
        subnets.create_subnet("home").unwrap();
        subnets.create_subnet("work").unwrap();
        subnets.save().unwrap();

        let src_perch = perch::resolve_perch_path("ling", ParentHint::Infer);
        std::fs::create_dir_all(&src_perch).unwrap();
        let mut rec = spt_store::info::InfoJson::new("ling", "t", 4242, "sid", "live_agent");
        rec.adapter = Some("mock".into());
        spt_store::info::write_info(&src_perch, &rec).unwrap();
        let cs = ContextStore::open_or_init().unwrap();
        let live = cs.live_context_path("ling").unwrap();
        std::fs::write(&live, "mind").unwrap();
        cs.commit_live("ling", "v1").unwrap();

        assert_eq!(cmd_fork("ling", "ling-work", "work", false), 0);
        let new_perch = perch::resolve_perch_path("ling-work", ParentHint::Infer);
        let new_rec = spt_store::info::read_info(&new_perch).unwrap();
        assert_eq!(new_rec.home_subnet.as_deref(), Some("work"), "the fork's home = target");
        assert_eq!(new_rec.adapter.as_deref(), Some("mock"), "adapter carries");
        assert_eq!(
            cs.branch_store().read_at_tip(&agent_branch("ling-work"), LIVE_CONTEXT_FILE).unwrap(),
            Some(b"mind".to_vec()),
            "one-time mind copy landed"
        );
        let vis = spt_store::visibility::VisibilityStore::load();
        assert_eq!(vis.sync_subnets("ling-work"), Some(&["work".to_string()][..]));

        // Refusal classes (each before any mutation).
        assert_eq!(cmd_fork("ling", "ling", "work", false), 1, "same id on one node");
        assert_eq!(cmd_fork("ling", "ling-work", "work", false), 1, "locally taken");
        assert_eq!(cmd_fork("ling", "other", "ghost", false), 1, "non-member target");
        assert_eq!(cmd_fork("ghost", "other", "work", false), 1, "missing source (M5 arm)");
        // Target-registry collision: ANY holder (even a resting one) owns it.
        let snap_dir = perch::identity_dir().join("registry");
        std::fs::create_dir_all(&snap_dir).unwrap();
        let mut reg = spt_net::net::registry::SubnetRegistry::new();
        reg.merge_instance(
            "claimed",
            spt_net::net::registry::Instance {
                node: "bb22".into(),
                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
            },
        );
        std::fs::write(snap_dir.join("work.json"), serde_json::to_string(&reg).unwrap())
            .unwrap();
        assert_eq!(cmd_fork("ling", "claimed", "work", false), 1, "target holder owns the name");

        // delete-source removes exactly the source.
        assert_eq!(cmd_fork("ling", "ling-cold", "work", true), 0);
        assert!(spt_store::info::read_info(&src_perch).is_none(), "source perch gone");
        assert!(
            !cs.branch_store().has_branch(&agent_branch("ling")).unwrap(),
            "source mind gone"
        );
        assert!(spt_store::info::read_info(&new_perch).is_some(), "earlier fork untouched");
        assert!(
            cs.branch_store().has_branch(&agent_branch("ling-cold")).unwrap(),
            "the deleting fork's own mind stands"
        );
    }

    // ── pair show-totp ─────────────────────────────────────────────────────────

assert!(parse(&["spt", "ring"]).is_err()); // missing required target

+    // [unit->REQ-INST-15] the fork command end to end on one node: copies

+    // the mind + mints the perch homed in the TARGET (adapter carried, sync

+    // scope seeded), refuses every collision class before mutating anything,

+    // and --delete-source removes exactly the source.

+    fn fork_forks_homes_and_deletes_exactly_the_source() {

+        use spt_store::contextstore::{agent_branch, ContextStore, LIVE_CONTEXT_FILE};

+        let _h = crate::testutil::isolated_home();

+        let mut subnets = SubnetStore::load();

+        subnets.create_subnet("home").unwrap();

+        subnets.create_subnet("work").unwrap();

+        let src_perch = perch::resolve_perch_path("ling", ParentHint::Infer);

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

+        let mut rec = spt_store::info::InfoJson::new("ling", "t", 4242, "sid", "live_agent");

+        rec.adapter = Some("mock".into());

+        spt_store::info::write_info(&src_perch, &rec).unwrap();

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

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

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

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

+        assert_eq!(cmd_fork("ling", "ling-work", "work", false), 0);

+        let new_perch = perch::resolve_perch_path("ling-work", ParentHint::Infer);

+        let new_rec = spt_store::info::read_info(&new_perch).unwrap();

+        assert_eq!(new_rec.home_subnet.as_deref(), Some("work"), "the fork's home = target");

+        assert_eq!(new_rec.adapter.as_deref(), Some("mock"), "adapter carries");

+            cs.branch_store().read_at_tip(&agent_branch("ling-work"), LIVE_CONTEXT_FILE).unwrap(),

+            "one-time mind copy landed"

+        let vis = spt_store::visibility::VisibilityStore::load();

+        assert_eq!(vis.sync_subnets("ling-work"), Some(&["work".to_string()][..]));

+        // Refusal classes (each before any mutation).

+        assert_eq!(cmd_fork("ling", "ling", "work", false), 1, "same id on one node");

+        assert_eq!(cmd_fork("ling", "ling-work", "work", false), 1, "locally taken");

+        assert_eq!(cmd_fork("ling", "other", "ghost", false), 1, "non-member target");

+        assert_eq!(cmd_fork("ghost", "other", "work", false), 1, "missing source (M5 arm)");

+        // Target-registry collision: ANY holder (even a resting one) owns it.

+        let snap_dir = perch::identity_dir().join("registry");

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

+        let mut reg = spt_net::net::registry::SubnetRegistry::new();

+            spt_net::net::registry::Instance {

+                status: spt_net::net::registry::Status::Dormant,

+        std::fs::write(snap_dir.join("work.json"), serde_json::to_string(&reg).unwrap())

+        assert_eq!(cmd_fork("ling", "claimed", "work", false), 1, "target holder owns the name");

+        // delete-source removes exactly the source.

+        assert_eq!(cmd_fork("ling", "ling-cold", "work", true), 0);

+        assert!(spt_store::info::read_info(&src_perch).is_none(), "source perch gone");

+            !cs.branch_store().has_branch(&agent_branch("ling")).unwrap(),

+        assert!(spt_store::info::read_info(&new_perch).is_some(), "earlier fork untouched");

+            cs.branch_store().has_branch(&agent_branch("ling-cold")).unwrap(),

+            "the deleting fork's own mind stands"

fn show_totp(args: &[&str]) -> PairCmd {


ADR-0010 doc tag + activate REQ-INST-15:


# Immutable home subnet; fork (copy-then-diverge) is the cross-subnet move, not re-home

# Immutable home subnet; fork (copy-then-diverge) is the cross-subnet move, not re-home

<!-- [doc->REQ-INST-15] -->
> **Delivered (M4-D9-5, 2026-06-04):** home assignment at creation
> (`spt_store::home` — auto-if-one / ask-if-many / local-only-until-first-join,
> carried forward across re-binds, no setter) + `sync_subnets = [home]`
> creation seeding + `spt fork <src> <new_id> --subnet <target>
> [--delete-source]` (one-time copy of both context tiers as fresh seed
> commits — copied-then-independent; join-time collision check against the
> target; the source untouched unless deleted). Same-node only in v1 — the
> remote arm composes with M5 instantiate-anywhere's consent gate.


The file C:\Users\decid\Documents\projects\spt-core\docs\adr\0010-immutable-home-subnet-fork-not-rehome.md has been updated successfully. (file state is current in your context — no need to Read it back)

# Immutable home subnet; fork (copy-then-diverge) is the cross-subnet move, not re-home

<!-- [doc->REQ-INST-15] -->
> **Delivered (M4-D9-5, 2026-06-04):** home assignment at creation
> (`spt_store::home` — auto-if-one / ask-if-many / local-only-until-first-join,
> carried forward across re-binds, no setter) + `sync_subnets = [home]`
> creation seeding + `spt fork <src> <new_id> --subnet <target>
> [--delete-source]` (one-time copy of both context tiers as fresh seed
> commits — copied-then-independent; join-time collision check against the
> target; the source untouched unless deleted). Same-node only in v1 — the
> remote arm composes with M5 instantiate-anywhere's consent gate.

# Immutable home subnet; fork (copy-then-diverge) is the cross-subnet move, not re-home

## Status

accepted (2026-06-02)

## Context

ADR-0006 gave each endpoint a **home subnet** — the default sync scope (subnet-exclusive mind replication) and the anchor that qualifies its bare name — assigned at creation. It did not say whether home can *change* over an endpoint's life. The obvious feature request is "re-home": move an existing agent's mind from subnet A to subnet B.

Re-home is deceptively expensive. Under node-global identity (ADR-0003) with per-subnet registries and `hidden ⟹ not synced` (ADR-0006), moving the home of a *live* identity raises migration semantics with no clean answer: does the mind already replicated on A's nodes get purged or stranded? what happens to in-flight sync and the `a-<id>` branch on A? is the bare-name qualification anchor changing mid-life (breaking addresses others have cached)? Each is a real, separately-decidable hazard, and together they make re-home a migration subsystem, not a setting.

## Decision

**Home subnet is immutable. There is no re-home.** To place an agent's mind in a different subnet, **`spt fork`** it: create a **new, identity-distinct endpoint** whose home is the target subnet, **seeded with a one-time copy** of the source's mind (the live + project Cross-node Psyche sync tiers). The fork and source **diverge** immediately — no ongoing sync. The source is untouched; it may be deleted afterward (`--delete-source`).

This sidesteps every re-home hazard: nothing migrates, nothing is stranded, the source's mind and addresses are unchanged until the user explicitly deletes it. The fork's id defaults to the source's bare name if free in the target subnet (the ADR-0006 join-time collision check; else prompt/rename); its adapter follows the normal creation rule.

A fork is deliberately **not an instance** and **not a rename**:
- **instance** — *same* identity, another node, *synced* mind (ADR-0003).
- **rename** — *same* identity, new label, rippled (ADR-0006).
- **fork** — *new* identity, another subnet, *copied-then-independent* mind (this ADR).

## Consequences

- Endpoint identity gains a clean lifecycle — create → (rename | fork) → delete — with no scope-mutation operation. Code may assume an endpoint's home never changes.
- "Move my agent to the work fleet" is a copy + optional delete, not an in-place migration; the two endpoints are independent afterward (intentional — divergent minds in divergent fleets).
- `fork` is a new v1/M4 endpoint operation alongside `rename`; it reuses the join-time collision check and the adapter/creation rules. Cross-node *fork* (source and target on different nodes) composes with instantiate-anywhere's consent gate.
- Records the rejected alternative (re-home) so it is not re-proposed; extends ADR-0006 (multi-subnet membership) and builds on ADR-0003 (node-anchored identity).

+> **Delivered (M4-D9-5, 2026-06-04):** home assignment at creation

+> (`spt_store::home` — auto-if-one / ask-if-many / local-only-until-first-join,

+> carried forward across re-binds, no setter) + `sync_subnets = [home]`

+> creation seeding + `spt fork <src> <new_id> --subnet <target>

+> [--delete-source]` (one-time copy of both context tiers as fresh seed

+> commits — copied-then-independent; join-time collision check against the

+> target; the source untouched unless deleted). Same-node only in v1 — the

+> remote arm composes with M5 instantiate-anywhere's consent gate.


id = "REQ-INST-15"
title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
required_stages = []

id = "REQ-INST-15"
title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
required_stages = ["doc", "impl", "unit"]  # activated M4-D9-5: spt-store::home (assign_home matrix: sole auto / several refuse-and-qualify / unpaired local-only; stamp_creation_fields = the ONE creation seam shared by spt ready + api listen/bind — NEW assigns hard + seeds sync_subnets=[home], REVIVE carries home/adapter/blurb/knobs forward so a re-bind never wipes them; adopt_for_unset = first-join adoption, sole-subnet only, never re-homes; NO setter — immutability by construction) + info.json home_subnet/adapter + ContextStore::fork_endpoint (one-time copy of live+project tiers as parentless seed commits — copied-then-diverged, no shared history) / remove_endpoint (--delete-source, exactly the source) + spt fork CLI (refusal-first gates: same-id, target membership, local + target-registry join-time collision; same-node v1, remote arm = M5 with instantiate-anywhere consent). doc = ADR-0010 delivered note; unit = assignment matrix + stamp news/revives + adoption + fork copies-then-diverges + collision classes + delete-source-exact


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

id = "REQ-INST-15"
title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
required_stages = []

id = "REQ-INST-15"
title = "Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)"
required_stages = ["doc", "impl", "unit"]  # activated M4-D9-5: spt-store::home (assign_home matrix: sole auto / several refuse-and-qualify / unpaired local-only; stamp_creation_fields = the ONE creation seam shared by spt ready + api listen/bind — NEW assigns hard + seeds sync_subnets=[home], REVIVE carries home/adapter/blurb/knobs forward so a re-bind never wipes them; adopt_for_unset = first-join adoption, sole-subnet only, never re-homes; NO setter — immutability by construction) + info.json home_subnet/adapter + ContextStore::fork_endpoint (one-time copy of live+project tiers as parentless seed commits — copied-then-diverged, no shared history) / remove_endpoint (--delete-source, exactly the source) + spt fork CLI (refusal-first gates: same-id, target membership, local + target-registry join-time collision; same-node v1, remote arm = M5 with instantiate-anywhere consent). doc = ADR-0010 delivered note; unit = assignment matrix + stamp news/revives + adoption + fork copies-then-diverges + collision classes + delete-source-exact

+required_stages = ["doc", "impl", "unit"]  # activated M4-D9-5: spt-store::home (assign_home matrix: sole auto / several refuse-and-qualify / unpaired local-only; stamp_creation_fields = the ONE creation seam shared by spt ready + api listen/bind — NEW assigns hard + seeds sync_subnets=[home], REVIVE carries home/adapter/blurb/knobs forward so a re-bind never wipes them; adopt_for_unset = first-join adoption, sole-subnet only, never re-homes; NO setter — immutability by construction) + info.json home_subnet/adapter + ContextStore::fork_endpoint (one-time copy of live+project tiers as parentless seed commits — copied-then-diverged, no shared history) / remove_endpoint (--delete-source, exactly the source) + spt fork CLI (refusal-first gates: same-id, target membership, local + target-registry join-time collision; same-node v1, remote arm = M5 with instantiate-anywhere consent). doc = ADR-0010 delivered note; unit = assignment matrix + stamp news/revives + adoption + fork copies-then-diverges + collision classes + delete-source-exact

title = "Off-node remote-drive detection + file transfer"


PowerShell

cargo build --workspace --all-targets 2>&1 | Select-String -Pattern "^error" -Context 0,6 | Select-Object -First 12; "BUILD=$LASTEXITCODE"

Build all targets


cargo test --workspace 2>&1 | Select-String -Pattern "FAILED|failures:" | Select-Object -First 5; cargo test --workspace *> $null; "TESTS=$LASTEXITCODE"; cargo clippy --workspace --all-targets -- -D warnings *> $null; "CLIPPY=$LASTEXITCODE"; traceable-reqs check *> $null; "TRACE=$LASTEXITCODE"

Full gates for D9-5-2


<task-notification>
<task-id>b40rlg5w3</task-id>
<tool-use-id>toolu_01QT3nEF2R5vVfCw8WAgs7cB</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\b40rlg5w3.output</output-file>
<status>completed</status>
<summary>Background command "Wait for e69f59a run specifically" completed (exit code 0)</summary>
</task-notification>


test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 63 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.98s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.60s
test effect::tests::failed_effect_is_not_recorded_applied ... ok
TESTS=0
CLIPPY=101
TRACE=0


Prompt:
<task-notification>
<task-id>b40rlg5w3</task-id>
<tool-use-id>toolu_01QT3nEF2R5vVfCw8WAgs7cB</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\b40rlg5w3.output</output-file>
<status>completed</status>
<summary>Background command "Wait for e69f59a run specifically" completed (exit code 0)</summary>
</task-notification>


1	e69f59a completed success
2

e69f59a completed success


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-String -Pattern "^error" -Context 0,8 | Select-Object -First 12

Show clippy failure


Error: Exit code 101
> error: doc list item without indentation
     --> crates\spt-store\src\contextstore.rs:311:9
      |
  311 |     /// fresh seed commits on `a-<new>` / under `<new>/` in each project
      |         ^
      |
      = help: if this is supposed to be its own paragraph, add a blank line
      = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#doc_lazy_continuation
      = note: `-D clippy::doc-lazy-continuation` implied by `-D warnings`
> error: doc list item without indentation
     --> crates\spt-store\src\contextstore.rs:312:9
      |
  312 |     /// worktree (**no shared history, no ongoing sync**: a fork is a new
      |         ^
      |
      = help: if this is supposed to be its own paragraph, add a blank line
      = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#doc_lazy_continuation
  help: indent this line
> error: doc list item without indentation
     --> crates\spt-store\src\contextstore.rs:313:9
      |
  313 |     /// identity, not an instance). Conflict artifacts are deliberately not
      |         ^
      |
      = help: if this is supposed to be its own paragraph, add a blank line
      = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#doc_lazy_continuation
  help: indent this line
> error: doc list item without indentation
     --> crates\spt-store\src\contextstore.rs:314:9
      |
  314 |     /// copied — the fork starts clean. Refuses when `a-<new>` already exists
      |         ^
      |
      = help: if this is supposed to be its own paragraph, add a blank line
      = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#doc_lazy_continuation
  help: indent this line
> error: doc list item without indentation
     --> crates\spt-store\src\contextstore.rs:315:9
      |
  315 |     /// (never merge two minds — the rename rule); a source with no tracked
      |         ^
      |
      = help: if this is supposed to be its own paragraph, add a blank line
      = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#doc_lazy_continuation
  help: indent this line
> error: doc list item without indentation
     --> crates\spt-store\src\contextstore.rs:316:9
      |
  316 |     /// mind forks as an empty (perch-only) endpoint, `Ok`.
      |         ^
      |
      = help: if this is supposed to be its own paragraph, add a blank line
      = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#doc_lazy_continuation
  help: indent this line
> error: could not compile `spt-store` (lib) due to 6 previous errors
  warning: build failed, waiting for other jobs to finish...
> error: could not compile `spt-store` (lib test) due to 6 previous errors


D9-5-1 CI green. Clippy: leading `+` reads as list item. Fix doc:


Edit

    /// The context arm of `spt fork <src> <new>` (D9-5, REQ-INST-15 —
    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s live
    /// + project tiers, after which the two diverge — the copy lands as
    /// fresh seed commits on `a-<new>` / under `<new>/` in each project
    /// worktree (**no shared history, no ongoing sync**: a fork is a new
    /// identity, not an instance). Conflict artifacts are deliberately not
    /// copied — the fork starts clean. Refuses when `a-<new>` already exists
    /// (never merge two minds — the rename rule); a source with no tracked
    /// mind forks as an empty (perch-only) endpoint, `Ok`.

    /// The context arm of `spt fork <src> <new>` (D9-5, REQ-INST-15 —
    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s
    /// live and project tiers, after which the two diverge — the copy lands
    /// as fresh seed commits on `a-<new>` / under `<new>/` in each project
    /// worktree (**no shared history, no ongoing sync**: a fork is a new
    /// identity, not an instance). Conflict artifacts are deliberately not
    /// copied — the fork starts clean. Refuses when `a-<new>` already exists
    /// (never merge two minds — the rename rule); a source with no tracked
    /// mind forks as an empty (perch-only) endpoint, `Ok`.


    /// The context arm of `spt fork <src> <new>` (D9-5, REQ-INST-15 —
    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s live
    /// + project tiers, after which the two diverge — the copy lands as
    /// fresh seed commits on `a-<new>` / under `<new>/` in each project
    /// worktree (**no shared history, no ongoing sync**: a fork is a new
    /// identity, not an instance). Conflict artifacts are deliberately not
    /// copied — the fork starts clean. Refuses when `a-<new>` already exists
    /// (never merge two minds — the rename rule); a source with no tracked
    /// mind forks as an empty (perch-only) endpoint, `Ok`.

    /// The context arm of `spt fork <src> <new>` (D9-5, REQ-INST-15 —
    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s
    /// live and project tiers, after which the two diverge — the copy lands
    /// as fresh seed commits on `a-<new>` / under `<new>/` in each project
    /// worktree (**no shared history, no ongoing sync**: a fork is a new
    /// identity, not an instance). Conflict artifacts are deliberately not
    /// copied — the fork starts clean. Refuses when `a-<new>` already exists
    /// (never merge two minds — the rename rule); a source with no tracked
    /// mind forks as an empty (perch-only) endpoint, `Ok`.

-    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s live

-    /// + project tiers, after which the two diverge — the copy lands as

-    /// fresh seed commits on `a-<new>` / under `<new>/` in each project

+    /// ADR-0010): seed `new`'s mind with a **one-time copy** of `src`'s

+    /// live and project tiers, after which the two diverge — the copy lands

+    /// as fresh seed commits on `a-<new>` / under `<new>/` in each project


PowerShell

cargo clippy --workspace --all-targets -- -D warnings *> $null; "CLIPPY=$LASTEXITCODE"; cargo test --workspace *> $null; "TESTS=$LASTEXITCODE"; traceable-reqs check *> $null; "TRACE=$LASTEXITCODE"

Re-run gates


CLIPPY=0
TESTS=0
TRACE=0


All green. Commit + push D9-5-2:


git add -A; git commit -m @'
feat(store,spt): M4-D9-5-2 spt fork — copy-then-diverge (REQ-INST-15 activated)

The fork half of ADR-0010 (home is immutable; fork is the cross-subnet
move, never a re-home):

- ContextStore::fork_endpoint — seed the fork's mind with a ONE-TIME copy
  of the source's live and project tiers, landing as fresh parentless
  seed commits (no shared history, no ongoing sync: a fork is a new
  identity, not an instance). A post-fork source write never reaches the
  fork. Conflict artifacts are not copied (the fork starts clean); an
  existing target mind refuses (never merge two minds).
- ContextStore::remove_endpoint — the --delete-source arm: the a- branch
  + worktree and every project slice of EXACTLY the source; the fork and
  other agents untouched; idempotent.
- spt fork <src> <new_id> --subnet <target> [--delete-source] — gates
  refusal-first so a refused fork leaves no half-fork: charset, same-id
  (one perch per name per node — the same-name fork rides the M5 remote
  arm), target membership, local + target-registry join-time collision
  (ANY holder owns the name, resting included). Then: context copy,
  perch mint homed in the TARGET (adapter + resource blurb carried),
  sync_subnets = [target] seeded, delete-source LAST (only after the
  fork is whole).
- ADR-0010 carries the delivered note (doc evidence);
  REQ-INST-15 -> [doc, impl, unit].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@; git push origin dev-freeform

Commit and push D9-5-2


warning: in the working copy of 'crates/spt-store/src/contextstore.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0010-immutable-home-subnet-fork-not-rehome.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 0b30dea] feat(store,spt): M4-D9-5-2 spt fork — copy-then-diverge (REQ-INST-15 activated)
 4 files changed, 441 insertions(+), 1 deletion(-)
To https://github.com/SaberMage/spt-core.git
   e69f59a..0b30dea  dev-freeform -> dev-freeform


Bash

sha=$(git rev-parse HEAD); until gh run list --branch dev-freeform --limit 3 --json status,conclusion,headSha --jq ".[] | select(.headSha==\"$sha\")" | grep -q '"status":"completed"'; do sleep 30; done; gh run list --branch dev-freeform --limit 3 --json status,conclusion,headSha --jq ".[] | select(.headSha==\"$sha\") | \"\(.headSha[0:7]) \(.status) \(.conclusion)\""

Wait for D9-5-2 CI run (sha-pinned)


Command running in background with ID: bp0q1si53. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bp0q1si53.output. You will be notified when it completes. To check interim output, use Read on that file path.


D9-5-2 pushed (0b30dea), watcher armed. When green: plan-status update for D9-3/4/5, commune, then self-clear before D9-6 (two-host rig work deserves fresh context).


Check the D9-3 budget CI run watcher (task b27lewgwl); if green, push D9-4 (already committed-ready in working tree) and continue D9-5 per M4-D9-PLAN.


Claude resuming /loop wakeup (Jun 4 6:39am)


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


Stale wakeup — that work already completed (D9-3 green, D9-4 pushed + green, D9-5-1 green, D9-5-2 pushed). Check the live watcher:


<system-reminder>Warning: the file exists but is shorter than the provided offset (1). The file has 1 lines.</system-reminder>


Edit

**Status:** D9-2 COMPLETE 2026-06-04 (three CI-green sub-slices; REQ-INST-3 and
REQ-INST-4 activated at `[impl, unit]`):

**Status:** D9-5 COMPLETE 2026-06-04 (D9-3, D9-4, D9-5 all CI-green; REQ-INST-3 at
`[doc, impl, unit]`, REQ-INST-14 and REQ-INST-15 activated at `[doc, impl, unit]`):
- D9-3 `785002e`+`6f61a89`+`650c7fb` — dormancy budget measured on BOTH rig hosts
  (the `[budget]` commit-message CI gate runs the env-gated harness
  `tests/budget.rs` on both runners): HFENDULEAM shell seat 7.6 MiB / claude seat
  ~308 MiB / zero idle CPU; gravity shell seat 1.7 MiB; suspended residual = the
  on-disk record. `docs/DORMANCY-BUDGET.md` locks the policy (warm default
  CONFIRMED; auto-suspend opt-in default OFF on the D9-2 knob chain; constrained
  nodes default the node leg ON); ADR-0003 red-team #9 CLOSED.
- D9-4 `0ea318b` — resource advertisement: `resources: Option<String>` on the
  registry `Instance` (additive, rides the lease + `RegistryUpdate` untouched),
  `resource_projection` over visible+routable rows (the exclusion closure
  resolution uses), both-authored sourcing (`info.json` via `spt resources set`
  wins; `daemon.json` `resources_blurb` node seed), `spt resources set/show/list`.
- D9-5 `e69f59a`+`0b30dea` — immutable home subnet (`spt_store::home`:
  auto-if-one / ask-if-many / local-only-until-first-join; ONE shared creation
  seam for `spt ready` + `api listen`/`bind` with carry-forward across re-binds;
  first-join adoption via the CLI create path + the advertisement-scan sweep; NO
  setter) + `sync_subnets = [home]` creation seeding + `spt fork` (one-time copy
  of both tiers as parentless seed commits — copied-then-diverged; refusal-first
  collision gates; `--delete-source` removes exactly the source; same-node v1,
  remote arm = M5).

D9-2 COMPLETE 2026-06-04 (three CI-green sub-slices; REQ-INST-3 and
REQ-INST-4 activated at `[impl, unit]`):


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

**Status:** D9-2 COMPLETE 2026-06-04 (three CI-green sub-slices; REQ-INST-3 and
REQ-INST-4 activated at `[impl, unit]`):

**Status:** D9-5 COMPLETE 2026-06-04 (D9-3, D9-4, D9-5 all CI-green; REQ-INST-3 at
`[doc, impl, unit]`, REQ-INST-14 and REQ-INST-15 activated at `[doc, impl, unit]`):
- D9-3 `785002e`+`6f61a89`+`650c7fb` — dormancy budget measured on BOTH rig hosts
  (the `[budget]` commit-message CI gate runs the env-gated harness
  `tests/budget.rs` on both runners): HFENDULEAM shell seat 7.6 MiB / claude seat
  ~308 MiB / zero idle CPU; gravity shell seat 1.7 MiB; suspended residual = the
  on-disk record. `docs/DORMANCY-BUDGET.md` locks the policy (warm default
  CONFIRMED; auto-suspend opt-in default OFF on the D9-2 knob chain; constrained
  nodes default the node leg ON); ADR-0003 red-team #9 CLOSED.
- D9-4 `0ea318b` — resource advertisement: `resources: Option<String>` on the
  registry `Instance` (additive, rides the lease + `RegistryUpdate` untouched),
  `resource_projection` over visible+routable rows (the exclusion closure
  resolution uses), both-authored sourcing (`info.json` via `spt resources set`
  wins; `daemon.json` `resources_blurb` node seed), `spt resources set/show/list`.
- D9-5 `e69f59a`+`0b30dea` — immutable home subnet (`spt_store::home`:
  auto-if-one / ask-if-many / local-only-until-first-join; ONE shared creation
  seam for `spt ready` + `api listen`/`bind` with carry-forward across re-binds;
  first-join adoption via the CLI create path + the advertisement-scan sweep; NO
  setter) + `sync_subnets = [home]` creation seeding + `spt fork` (one-time copy
  of both tiers as parentless seed commits — copied-then-diverged; refusal-first
  collision gates; `--delete-source` removes exactly the source; same-node v1,
  remote arm = M5).

D9-2 COMPLETE 2026-06-04 (three CI-green sub-slices; REQ-INST-3 and
REQ-INST-4 activated at `[impl, unit]`):

# M4-D9 — activation sweep + cross-node E2E + M4 closeout (JIT plan)

-**Status:** D9-2 COMPLETE 2026-06-04 (three CI-green sub-slices; REQ-INST-3 and

+**Status:** D9-5 COMPLETE 2026-06-04 (D9-3, D9-4, D9-5 all CI-green; REQ-INST-3 at

+`[doc, impl, unit]`, REQ-INST-14 and REQ-INST-15 activated at `[doc, impl, unit]`):

+- D9-3 `785002e`+`6f61a89`+`650c7fb` — dormancy budget measured on BOTH rig hosts

+  (the `[budget]` commit-message CI gate runs the env-gated harness

+  `tests/budget.rs` on both runners): HFENDULEAM shell seat 7.6 MiB / claude seat

+  ~308 MiB / zero idle CPU; gravity shell seat 1.7 MiB; suspended residual = the

+  on-disk record. `docs/DORMANCY-BUDGET.md` locks the policy (warm default

+  CONFIRMED; auto-suspend opt-in default OFF on the D9-2 knob chain; constrained

+  nodes default the node leg ON); ADR-0003 red-team #9 CLOSED.

+- D9-4 `0ea318b` — resource advertisement: `resources: Option<String>` on the

+  registry `Instance` (additive, rides the lease + `RegistryUpdate` untouched),

+  `resource_projection` over visible+routable rows (the exclusion closure

+  resolution uses), both-authored sourcing (`info.json` via `spt resources set`

+  wins; `daemon.json` `resources_blurb` node seed), `spt resources set/show/list`.

+- D9-5 `e69f59a`+`0b30dea` — immutable home subnet (`spt_store::home`:

+  auto-if-one / ask-if-many / local-only-until-first-join; ONE shared creation

+  seam for `spt ready` + `api listen`/`bind` with carry-forward across re-binds;

+  first-join adoption via the CLI create path + the advertisement-scan sweep; NO

+  setter) + `sync_subnets = [home]` creation seeding + `spt fork` (one-time copy

+  of both tiers as parentless seed commits — copied-then-diverged; refusal-first

+  collision gates; `--delete-source` removes exactly the source; same-node v1,

+D9-2 COMPLETE 2026-06-04 (three CI-green sub-slices; REQ-INST-3 and

- D9-2-1 `0315783` — the explicit resting-state machine (`spt-daemon::resting`: pure

transition table, dormancy-onset auto-suspend anchor, global→node→endpoint knob


Plan-status staged locally. Watcher bp0q1si53 will re-invoke on D9-5-2 run completion — then: commit plan status, commune, self-clear before D9-6 (two-host rig + runbook deserves fresh context).


<task-notification>
<task-id>bp0q1si53</task-id>
<tool-use-id>toolu_0176keLWrt1AVqGQrxaHr1v8</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\bp0q1si53.output</output-file>
<status>completed</status>
<summary>Background command "Wait for D9-5-2 CI run (sha-pinned)" completed (exit code 0)</summary>
</task-notification>


1	0b30dea completed success
2

0b30dea completed success


Updated task #6 status


PowerShell

git add M4-D9-PLAN.md; git commit -m @'
docs(plan): mark M4-D9-3/4/5 complete (budget measured, resources, home+fork)

REQ-INST-3 doc stage closed by DORMANCY-BUDGET.md (ADR-0003 #9);
REQ-INST-14 and REQ-INST-15 activated at [doc, impl, unit]. Remaining:
D9-6 two-host rig proof + gh-interim retirement, D9-7 closeout sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@; git push origin dev-freeform

Commit and push plan status


warning: in the working copy of 'M4-D9-PLAN.md', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 92fc261] docs(plan): mark M4-D9-3/4/5 complete (budget measured, resources, home+fork)
 1 file changed, 24 insertions(+), 1 deletion(-)
To https://github.com/SaberMage/spt-core.git
   0b30dea..92fc261  dev-freeform -> dev-freeform


Commune, then self-clear for D9-6:


Write

<live-context>
Generation 15. D9-2 THROUGH D9-5 ALL COMPLETE, CI-green-FINAL, plan-status @ 92fc261. D9-6 next (two-host rig proof), then D9-7 sweep → M4 closeout.

## Completed this generation (all CI-green-FINAL)
- D9-2 (a52dacb): resting-state model. spt-daemon/resting.rs pure table (detach/attention-shift→dormant; suspend from either live state; auto-suspend from dormancy-onset anchor; wake in place; replay=no-edge). info.json rest_state+dormant_since_ms atomic pair. Status::Suspended (addressable). advertise_local follows machine epoch-bumped. apply_event echo-BEFORE-flip (KH 3.3; loud failure persists state). Wake → resurface_at_boundary + freshness-pull marker identity/sync-pull-now (pump take_freshness_pull forces sync round). Production feeds: attach arm Wake/Detach; registry apply_feed_flips (flip = Updated ∧ →Active ∧ prev≠Active ∧ peer node — NEVER Inserted: restart re-learn safe); pulse recency GATED on rest state (resting seat never wins most-recently-active); spt suspend/wake local. Daemon-side echo = GATE-ARMED (arm_transition_echo; endpoint's own pulse runs bounded call — KH 7.4 by construction, stays unbound).
- D9-3 (785002e/6f61a89/650c7fb): dormancy budget. tests/budget.rs SPT_BUDGET=1 env-gated; CI [budget] commit-message gate runs BOTH runners (per-OS steps: bash linux / powershell windows — PS5.1 ANSI temp script broke on em-dash, ASCII-only now). Numbers: HFENDULEAM shell 7.6MiB/85h/0cpu, claude ~308MiB/244h/~0cpu; gravity shell 1.7MiB/4fd/0cpu; suspended residual = on-disk record only. docs/DORMANCY-BUDGET.md = policy lock (warm default CONFIRMED; auto-suspend opt-in OFF; constrained nodes node-leg ON). ADR-0003 #9 CLOSED. Brain grew spawn_session_pid.
- D9-4 (0ea318b): resource advertisement. Instance.resources Option<String> (additive serde; rides lease+RegistryUpdate untouched — blurb edit = ordinary epoch-bumped update). resource_projection over visible+routable rows (same exclusion closure as resolution; Offline skipped). Both-authored: info.json resources (spt resources set) wins, daemon.json resources_blurb node seed. spt resources set/show/list (list off identity/registry snapshots). DaemonConfig dropped Copy. Doc evidence in docs/STORAGE.md (CONTEXT.md NOT in scan roots — gotcha: traceable-reqs scan.roots = src/tests/docs/crates only).
- D9-5 (e69f59a/0b30dea): home + fork. spt-store/home.rs: assign_home matrix (sole auto / multi refuse-and-qualify / unpaired local-only); stamp_creation_fields = ONE shared creation seam (spt ready --subnet, api listen/bind --subnet; seed carries adapter on listen path, api --adapter on bind path) — NEW assigns hard + seeds sync_subnets=[home]; REVIVE carries home/adapter/resources/auto_suspend forward (establish_perch REWRITES info.json on re-bind — carry-forward is load-bearing, found during build); adopt_for_unset first-join adoption (sole-subnet only; CLI create-new immediate + advertise_local sweep within one cadence — no pairing-protocol threading). NO setter. ContextStore::fork_endpoint = one-time copy both tiers as parentless seed commits (copied-then-DIVERGED, no shared DAG); remove_endpoint = delete-source exact. spt fork refusal-first gates (same-id — one perch per name per node, same-name fork = M5 remote arm; target membership; local + target-registry collision, ANY status holds the name). InfoJson grew home_subnet+adapter.

## Reqs now active
REQ-INST-3 [doc,impl,unit] · REQ-INST-4 [impl,unit] · REQ-INST-14 [doc,impl,unit] · REQ-INST-15 [doc,impl,unit]. REQ-INST-6 stays [] (M5, seam doc'd). REQ-HAZARD-DAEMON-SCHED-NONBLOCKING stays [] (holds by construction via gate-arm; binds at per-agent-runtime fan-out).

## D9-6 next (two-host E2E + runbook + gh-interim retirement)
- SPT_TWO_HOST=1-gated int tests (skip silently; role/peer-addr via env): M4-PLAN §D9b ladder — pair → register → cross-node message → remote-drive → Psyche sync → update self-heal + notif cross-node leg (insert A→fires B; dismiss B→replicates A). Int tags: [int->REQ-NET-1], [int->REQ-INST-5], [int->REQ-UPD-1], [int->REQ-NOTIF-1] + pairing/registry as touched. Activate int stages ONLY after real rig run green.
- docs/TWO-HOST-RUNBOOK.md (HFENDULEAM ↔ gravity-linux procedure; evidence appendix from real run).
- Run on rig. NOTE: no SSH from HFENDULEAM to gravity (hostname unresolvable) — the [budget]-style commit-message-gated CI step pattern WORKS for driving gravity legs (proven D9-3); consider [twohost] tag gate or runbook manual procedure. gh runner on gravity is systemd, hfenduleam C:\actions-runner detached.
- Retire gh-repo interim: sister gh-repo-sync skill stands down; psyche-sync-setup remains for hub-mode seam but no longer THE sync path; note in CONTEXT.md.
- Then D9-7: traceable-reqs full audit (apply_brain_only no-production-caller note), ROADMAP M4 delivered, CONTEXT glossary amendments, plan-status COMPLETE, CI FINAL green.

## Workflow constraints, gotchas 1–10 + NEW gotchas, flake ledger, CI topology: carry all.
NEW gotchas this gen: (11) traceable-reqs scan.roots excludes repo-root *.md — doc tags must land under docs/; (12) Windows PowerShell 5.1 CI steps are ANSI — ASCII-only strings; (13) gh run list --limit 1 races a fresh push — pin watcher loops to the pushed sha.

## No Blockers
</live-context>
<project-context>
name=spt-core

M4-D9: D9-1..D9-5 COMPLETE CI-green (92fc261 plan-status). Production trigger loops, resting-state model (REQ-INST-3/4), dormancy budget measured + policy locked (ADR-0003 #9 closed, warm default confirmed), resource advertisement (REQ-INST-14), immutable home + spt fork (REQ-INST-15). Remaining: D9-6 two-host rig proof (SPT_TWO_HOST=1 int tests + TWO-HOST-RUNBOOK.md + real rig run + gh-interim retirement), D9-7 closeout sweep → M4 delivered. Conventions hold: no cargo fmt; evidence-only tags; traceable-reqs check repo root (scan roots = src/tests/docs/crates — repo-root md NOT scanned); clippy -D warnings; push slice → watch FINAL sha-pinned (gotcha #7+#13); commit trailer Claude Opus 4.8 (1M context).
</project-context>


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.claude\doyle-commune.md (file state is current in your context — no need to Read it back)

<live-context>
Generation 15. D9-2 THROUGH D9-5 ALL COMPLETE, CI-green-FINAL, plan-status @ 92fc261. D9-6 next (two-host rig proof), then D9-7 sweep → M4 closeout.

## Completed this generation (all CI-green-FINAL)
- D9-2 (a52dacb): resting-state model. spt-daemon/resting.rs pure table (detach/attention-shift→dormant; suspend from either live state; auto-suspend from dormancy-onset anchor; wake in place; replay=no-edge). info.json rest_state+dormant_since_ms atomic pair. Status::Suspended (addressable). advertise_local follows machine epoch-bumped. apply_event echo-BEFORE-flip (KH 3.3; loud failure persists state). Wake → resurface_at_boundary + freshness-pull marker identity/sync-pull-now (pump take_freshness_pull forces sync round). Production feeds: attach arm Wake/Detach; registry apply_feed_flips (flip = Updated ∧ →Active ∧ prev≠Active ∧ peer node — NEVER Inserted: restart re-learn safe); pulse recency GATED on rest state (resting seat never wins most-recently-active); spt suspend/wake local. Daemon-side echo = GATE-ARMED (arm_transition_echo; endpoint's own pulse runs bounded call — KH 7.4 by construction, stays unbound).
- D9-3 (785002e/6f61a89/650c7fb): dormancy budget. tests/budget.rs SPT_BUDGET=1 env-gated; CI [budget] commit-message gate runs BOTH runners (per-OS steps: bash linux / powershell windows — PS5.1 ANSI temp script broke on em-dash, ASCII-only now). Numbers: HFENDULEAM shell 7.6MiB/85h/0cpu, claude ~308MiB/244h/~0cpu; gravity shell 1.7MiB/4fd/0cpu; suspended residual = on-disk record only. docs/DORMANCY-BUDGET.md = policy lock (warm default CONFIRMED; auto-suspend opt-in OFF; constrained nodes node-leg ON). ADR-0003 #9 CLOSED. Brain grew spawn_session_pid.
- D9-4 (0ea318b): resource advertisement. Instance.resources Option<String> (additive serde; rides lease+RegistryUpdate untouched — blurb edit = ordinary epoch-bumped update). resource_projection over visible+routable rows (same exclusion closure as resolution; Offline skipped). Both-authored: info.json resources (spt resources set) wins, daemon.json resources_blurb node seed. spt resources set/show/list (list off identity/registry snapshots). DaemonConfig dropped Copy. Doc evidence in docs/STORAGE.md (CONTEXT.md NOT in scan roots — gotcha: traceable-reqs scan.roots = src/tests/docs/crates only).
- D9-5 (e69f59a/0b30dea): home + fork. spt-store/home.rs: assign_home matrix (sole auto / multi refuse-and-qualify / unpaired local-only); stamp_creation_fields = ONE shared creation seam (spt ready --subnet, api listen/bind --subnet; seed carries adapter on listen path, api --adapter on bind path) — NEW assigns hard + seeds sync_subnets=[home]; REVIVE carries home/adapter/resources/auto_suspend forward (establish_perch REWRITES info.json on re-bind — carry-forward is load-bearing, found during build); adopt_for_unset first-join adoption (sole-subnet only; CLI create-new immediate + advertise_local sweep within one cadence — no pairing-protocol threading). NO setter. ContextStore::fork_endpoint = one-time copy both tiers as parentless seed commits (copied-then-DIVERGED, no shared DAG); remove_endpoint = delete-source exact. spt fork refusal-first gates (same-id — one perch per name per node, same-name fork = M5 remote arm; target membership; local + target-registry collision, ANY status holds the name). InfoJson grew home_subnet+adapter.

## Reqs now active
REQ-INST-3 [doc,impl,unit] · REQ-INST-4 [impl,unit] · REQ-INST-14 [doc,impl,unit] · REQ-INST-15 [doc,impl,unit]. REQ-INST-6 stays [] (M5, seam doc'd). REQ-HAZARD-DAEMON-SCHED-NONBLOCKING stays [] (holds by construction via gate-arm; binds at per-agent-runtime fan-out).

## D9-6 next (two-host E2E + runbook + gh-interim retirement)
- SPT_TWO_HOST=1-gated int tests (skip silently; role/peer-addr via env): M4-PLAN §D9b ladder — pair → register → cross-node message → remote-drive → Psyche sync → update self-heal + notif cross-node leg (insert A→fires B; dismiss B→replicates A). Int tags: [int->REQ-NET-1], [int->REQ-INST-5], [int->REQ-UPD-1], [int->REQ-NOTIF-1] + pairing/registry as touched. Activate int stages ONLY after real rig run green.
- docs/TWO-HOST-RUNBOOK.md (HFENDULEAM ↔ gravity-linux procedure; evidence appendix from real run).
- Run on rig. NOTE: no SSH from HFENDULEAM to gravity (hostname unresolvable) — the [budget]-style commit-message-gated CI step pattern WORKS for driving gravity legs (proven D9-3); consider [twohost] tag gate or runbook manual procedure. gh runner on gravity is systemd, hfenduleam C:\actions-runner detached.
- Retire gh-repo interim: sister gh-repo-sync skill stands down; psyche-sync-setup remains for hub-mode seam but no longer THE sync path; note in CONTEXT.md.
- Then D9-7: traceable-reqs full audit (apply_brain_only no-production-caller note), ROADMAP M4 delivered, CONTEXT glossary amendments, plan-status COMPLETE, CI FINAL green.

## Workflow constraints, gotchas 1–10 + NEW gotchas, flake ledger, CI topology: carry all.
NEW gotchas this gen: (11) traceable-reqs scan.roots excludes repo-root *.md — doc tags must land under docs/; (12) Windows PowerShell 5.1 CI steps are ANSI — ASCII-only strings; (13) gh run list --limit 1 races a fresh push — pin watcher loops to the pushed sha.

## No Blockers
</live-context>
<project-context>
name=spt-core

M4-D9: D9-1..D9-5 COMPLETE CI-green (92fc261 plan-status). Production trigger loops, resting-state model (REQ-INST-3/4), dormancy budget measured + policy locked (ADR-0003 #9 closed, warm default confirmed), resource advertisement (REQ-INST-14), immutable home + spt fork (REQ-INST-15). Remaining: D9-6 two-host rig proof (SPT_TWO_HOST=1 int tests + TWO-HOST-RUNBOOK.md + real rig run + gh-interim retirement), D9-7 closeout sweep → M4 delivered. Conventions hold: no cargo fmt; evidence-only tags; traceable-reqs check repo root (scan roots = src/tests/docs/crates — repo-root md NOT scanned); clippy -D warnings; push slice → watch FINAL sha-pinned (gotcha #7+#13); commit trailer Claude Opus 4.8 (1M context).
</project-context>


Skill

D9-1 through D9-5 complete and CI-green-FINAL (plan-status @ 92fc261); commune dropped. Resume M4-D9 at D9-6 per M4-D9-PLAN.md: SPT_TWO_HOST=1-gated int tests (pair → register → cross-node message → remote-drive → Psyche sync → update self-heal + notif cross-node leg), docs/TWO-HOST-RUNBOOK.md, real rig run (no SSH to gravity — use the [budget]-style commit-message-gated CI step pattern proven in D9-3), gh-repo interim retirement, then D9-7 closeout sweep.


Launching skill: self-clear