1	//! Worker perches: `api worker-start` / `worker-stop` / `worker-poll`, plus the
2	//! **cascade-wipe guard** (KNOWN-HAZARDS 6.3).
3	//!
4	//! A worker is a nested child perch under a parent agent
5	//! (`owlery/<parent>/nested/<worker>/`), addressable like any perch and routed
6	//! through the *single* path resolver (REQ-HAZARD-SINGLE-PATH-SOURCE / WORKER-
7	//! PATH — no divergent sibling layouts).
8	//!
9	//! ## Cascade-wipe guard (6.3, REQ-HAZARD-CASCADE-WIPE-GUARD)
10	//! A destructive parent teardown must never hard-delete a parent that still
11	//! hosts **live** children. "Live" here = the child still carries its `ready`
12	//! marker (the soft-cleanup signal; `worker-stop` removes it). When live
13	//! children exist, the caller soft-cleans and surfaces their paths instead of
14	//! recursively deleting in-flight work. [`has_live_children`] /
15	//! [`live_child_paths`] are the guard primitives T7's `session-end` consults.
16	//
17	// [impl->REQ-API-2]
18	// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]
19
20	use spt_store::info::{self, InfoJson};
21	use spt_store::perch::{self, ParentHint};
22	use spt_store::spool;
23
24	use super::auth;
25	use super::EXIT_REFUSED;
26
27	/// `api worker-start <parent> <id>` — create a nested worker perch under
28	/// `parent`. Resolves with an Explicit parent so the layout is unambiguous even
29	/// if the id's own suffix would infer a different one.
30	// [impl->REQ-API-2]
31	pub fn cmd_worker_start(parent: &str, id: &str) -> i32 {
32	    let perch_path = perch::resolve_perch_path(id, ParentHint::Explicit(parent));
33	    if let Err(e) = std::fs::create_dir_all(&perch_path) {
34	        eprintln!("WORKER_START_FAIL:{id}: {e}");
35	        return EXIT_REFUSED;
36	    }
37	    let rec = InfoJson::new(id, &now_stamp(), std::process::id(), "", "worker");
38	    if let Err(e) = info::write_info(&perch_path, &rec) {
39	        eprintln!("WORKER_START_FAIL:{id}: {e}");
40	        return EXIT_REFUSED;
41	    }
42	    // ready marker = the live-child signal the cascade-wipe guard checks.
43	    let _ = std::fs::write(
44	        perch::resolve_ready_file(id, ParentHint::Explicit(parent)),
45	        "",
46	    );
47	    let token = auth::mint_token();
48	    let _ = auth::write_token(&perch_path, &token);
49	    eprintln!("WORKER_STARTED:{id} under {parent} token={token}");
50	    0
51	}
52
53	/// `api worker-stop <id>` — soft-clean a worker (REQ-HAZARD-SOFT-CLEANUP):
54	/// remove its `ready` marker so it no longer counts as a live child; the
55	/// `info.json` + spool are preserved. Idempotent.
56	// [impl->REQ-API-2]
57	pub fn cmd_worker_stop(id: &str) -> i32 {
58	    let removed = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer)).is_ok();
59	    if removed {
60	        eprintln!("WORKER_STOPPED:{id}");
61	    } else {
62	        eprintln!("WORKER_STOPPED:{id} (no ready marker)");
63	    }
64	    0
65	}
66
67	/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), framed
68	/// to stdout. Non-deferred only unless `include_deferred`.
69	// [impl->REQ-API-2]
70	pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
71	    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
72	    let drained = if include_deferred {
73	        spool::drain_all_at(&perch_path)
74	    } else {
75	        spool::drain_non_deferred_at(&perch_path)
76	    };
77	    for msg in drained.unwrap_or_default() {
78	        use std::io::Write;
79	        print!("{msg}");
80	        let _ = std::io::stdout().flush();
81	    }
82	    0
83	}
84
85	/// The paths of `parent_id`'s **live** nested children (those still carrying a
86	/// `ready` marker). Empty ⇒ the parent is safe to hard-delete.
87	// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]
88	pub fn live_child_paths(parent_id: &str) -> Vec<std::path::PathBuf> {
89	    let nested = perch::resolve_perch_path(parent_id, ParentHint::Infer).join("nested");
90	    let mut live = Vec::new();
91	    let Ok(entries) = std::fs::read_dir(&nested) else {
92	        return live;
93	    };
94	    for entry in entries.flatten() {
95	        let child = entry.path();
96	        if !child.is_dir() {
97	            continue;
98	        }
99	        // A child is live while its ready marker exists (info.json present =
100	        // it's a real perch; ready = not yet soft-cleaned).
101	        if info::read_info(&child).is_some() && child.join("ready").exists() {
102	            live.push(child);
103	        }
104	    }
105	    live
106	}
107
108	fn now_stamp() -> String {
109	    std::time::SystemTime::now()
110	        .duration_since(std::time::UNIX_EPOCH)
111	        .map(|d| d.as_secs().to_string())
112	        .unwrap_or_else(|_| "0".to_string())
113	}
114
115	#[cfg(test)]
116	mod tests {
117	    use super::*;
118	    use crate::testutil::isolated_home;
119
120	    fn establish_parent(id: &str) {
121	        let path = perch::resolve_perch_path(id, ParentHint::Infer);
122	        std::fs::create_dir_all(&path).unwrap();
123	        info::write_info(
124	            &path,
125	            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),
126	        )
127	        .unwrap();
128	    }
129
130	    #[test] // [unit->REQ-API-2] a worker nests under its parent and is addressable.
131	    fn worker_start_nests_under_parent() {
132	        let _h = isolated_home();
133	        establish_parent("alice");
134	        assert_eq!(cmd_worker_start("alice", "alice-w1"), 0);
135
136	        let expected = perch::resolve_perch_path("alice", ParentHint::Infer)
137	            .join("nested")
138	            .join("alice-w1");
139	        assert!(expected.join("info.json").exists());
140	        assert!(expected.join("ready").exists());
141	        // Inferred resolution agrees with the explicit nesting.
142	        assert_eq!(perch::resolve_perch_path("alice-w1", ParentHint::Infer), expected);
143	    }
144
145	    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] a parent with a live worker
146	    // reports children; stopping the worker clears the guard.
147	    fn cascade_guard_tracks_live_children() {
148	        let _h = isolated_home();
149	        establish_parent("alice");
150	        assert!(live_child_paths("alice").is_empty(), "no children yet");
151
152	        cmd_worker_start("alice", "alice-w1");
153	        assert!(!live_child_paths("alice").is_empty(), "live worker must be seen");
154	        assert_eq!(live_child_paths("alice").len(), 1);
155
156	        // Soft-stop the worker → ready marker gone → no longer a live child.
157	        assert_eq!(cmd_worker_stop("alice-w1"), 0);
158	        assert!(live_child_paths("alice").is_empty(), "soft-stopped worker is not live");
159	        // The record is preserved (soft cleanup), only the ready marker removed.
160	        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
161	        assert!(child.join("info.json").exists());
162	        assert!(!child.join("ready").exists());
163	    }
164
165	    #[test] // [unit->REQ-API-2] worker-poll drains the worker's own spool.
166	    fn worker_poll_drains_spool() {
167	        let _h = isolated_home();
168	        establish_parent("alice");
169	        cmd_worker_start("alice", "alice-w1");
170	        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
171	        spool::spool_message_at(&child, "alice", "task result").unwrap();
172
173	        let perch_path = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
174	        let got = spool::drain_non_deferred_at(&perch_path).unwrap();
175	        assert_eq!(got.len(), 1);
176	        // And the command path returns 0.
177	        cmd_worker_start("alice", "alice-w2");
178	        assert_eq!(cmd_worker_poll("alice-w2", false), 0);
179	    }
180
181	    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] two workers: guard holds until
182	    // the last live child is stopped.
183	    fn guard_clears_only_when_all_children_stopped() {
184	        let _h = isolated_home();
185	        establish_parent("alice");
186	        cmd_worker_start("alice", "alice-w1");
187	        cmd_worker_start("alice", "alice-w2");

... [7 lines truncated] ...


Edit

        cmd_worker_stop("alice-w2");
        assert!(!has_live_children("alice"));

        cmd_worker_stop("alice-w2");
        assert!(live_child_paths("alice").is_empty());


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

        cmd_worker_stop("alice-w2");
        assert!(live_child_paths("alice").is_empty());

//! Worker perches: `api worker-start` / `worker-stop` / `worker-poll`, plus the
//! **cascade-wipe guard** (KNOWN-HAZARDS 6.3).
//!
//! A worker is a nested child perch under a parent agent
//! (`owlery/<parent>/nested/<worker>/`), addressable like any perch and routed
//! through the *single* path resolver (REQ-HAZARD-SINGLE-PATH-SOURCE / WORKER-
//! PATH — no divergent sibling layouts).
//!
//! ## Cascade-wipe guard (6.3, REQ-HAZARD-CASCADE-WIPE-GUARD)
//! A destructive parent teardown must never hard-delete a parent that still
//! hosts **live** children. "Live" here = the child still carries its `ready`
//! marker (the soft-cleanup signal; `worker-stop` removes it). When live
//! children exist, the caller soft-cleans and surfaces their paths instead of
//! recursively deleting in-flight work. [`has_live_children`] /
//! [`live_child_paths`] are the guard primitives T7's `session-end` consults.
//
// [impl->REQ-API-2]
// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]

use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;

use super::auth;
use super::EXIT_REFUSED;

/// `api worker-start <parent> <id>` — create a nested worker perch under
/// `parent`. Resolves with an Explicit parent so the layout is unambiguous even
/// if the id's own suffix would infer a different one.
// [impl->REQ-API-2]
pub fn cmd_worker_start(parent: &str, id: &str) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Explicit(parent));
    if let Err(e) = std::fs::create_dir_all(&perch_path) {
        eprintln!("WORKER_START_FAIL:{id}: {e}");
        return EXIT_REFUSED;
    }
    let rec = InfoJson::new(id, &now_stamp(), std::process::id(), "", "worker");
    if let Err(e) = info::write_info(&perch_path, &rec) {
        eprintln!("WORKER_START_FAIL:{id}: {e}");
        return EXIT_REFUSED;
    }
    // ready marker = the live-child signal the cascade-wipe guard checks.
    let _ = std::fs::write(
        perch::resolve_ready_file(id, ParentHint::Explicit(parent)),
        "",
    );
    let token = auth::mint_token();
    let _ = auth::write_token(&perch_path, &token);
    eprintln!("WORKER_STARTED:{id} under {parent} token={token}");
    0
}

/// `api worker-stop <id>` — soft-clean a worker (REQ-HAZARD-SOFT-CLEANUP):
/// remove its `ready` marker so it no longer counts as a live child; the
/// `info.json` + spool are preserved. Idempotent.
// [impl->REQ-API-2]
pub fn cmd_worker_stop(id: &str) -> i32 {
    let removed = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer)).is_ok();
    if removed {
        eprintln!("WORKER_STOPPED:{id}");
    } else {
        eprintln!("WORKER_STOPPED:{id} (no ready marker)");
    }
    0
}

/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), framed
/// to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    for msg in drained.unwrap_or_default() {
        use std::io::Write;
        print!("{msg}");
        let _ = std::io::stdout().flush();
    }
    0
}

/// The paths of `parent_id`'s **live** nested children (those still carrying a
/// `ready` marker). Empty ⇒ the parent is safe to hard-delete.
// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]
pub fn live_child_paths(parent_id: &str) -> Vec<std::path::PathBuf> {
    let nested = perch::resolve_perch_path(parent_id, ParentHint::Infer).join("nested");
    let mut live = Vec::new();
    let Ok(entries) = std::fs::read_dir(&nested) else {
        return live;
    };
    for entry in entries.flatten() {
        let child = entry.path();
        if !child.is_dir() {
            continue;
        }
        // A child is live while its ready marker exists (info.json present =
        // it's a real perch; ready = not yet soft-cleaned).
        if info::read_info(&child).is_some() && child.join("ready").exists() {
            live.push(child);
        }
    }
    live
}

fn now_stamp() -> String {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".to_string())
}

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

    fn establish_parent(id: &str) {
        let path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&path).unwrap();
        info::write_info(
            &path,
            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),
        )
        .unwrap();
    }

    #[test] // [unit->REQ-API-2] a worker nests under its parent and is addressable.
    fn worker_start_nests_under_parent() {
        let _h = isolated_home();
        establish_parent("alice");
        assert_eq!(cmd_worker_start("alice", "alice-w1"), 0);

        let expected = perch::resolve_perch_path("alice", ParentHint::Infer)
            .join("nested")
            .join("alice-w1");
        assert!(expected.join("info.json").exists());
        assert!(expected.join("ready").exists());
        // Inferred resolution agrees with the explicit nesting.
        assert_eq!(perch::resolve_perch_path("alice-w1", ParentHint::Infer), expected);
    }

    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] a parent with a live worker
    // reports children; stopping the worker clears the guard.
    fn cascade_guard_tracks_live_children() {
        let _h = isolated_home();
        establish_parent("alice");
        assert!(live_child_paths("alice").is_empty(), "no children yet");

        cmd_worker_start("alice", "alice-w1");
        assert!(!live_child_paths("alice").is_empty(), "live worker must be seen");
        assert_eq!(live_child_paths("alice").len(), 1);

        // Soft-stop the worker → ready marker gone → no longer a live child.
        assert_eq!(cmd_worker_stop("alice-w1"), 0);
        assert!(live_child_paths("alice").is_empty(), "soft-stopped worker is not live");
        // The record is preserved (soft cleanup), only the ready marker removed.
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        assert!(child.join("info.json").exists());
        assert!(!child.join("ready").exists());
    }

    #[test] // [unit->REQ-API-2] worker-poll drains the worker's own spool.
    fn worker_poll_drains_spool() {
        let _h = isolated_home();
        establish_parent("alice");
        cmd_worker_start("alice", "alice-w1");
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        spool::spool_message_at(&child, "alice", "task result").unwrap();

        let perch_path = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        let got = spool::drain_non_deferred_at(&perch_path).unwrap();
        assert_eq!(got.len(), 1);
        // And the command path returns 0.
        cmd_worker_start("alice", "alice-w2");
        assert_eq!(cmd_worker_poll("alice-w2", false), 0);
    }

    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] two workers: guard holds until
    // the last live child is stopped.
    fn guard_clears_only_when_all_children_stopped() {
        let _h = isolated_home();
        establish_parent("alice");
        cmd_worker_start("alice", "alice-w1");
        cmd_worker_start("alice", "alice-w2");
        assert_eq!(live_child_paths("alice").len(), 2);
        cmd_worker_stop("alice-w1");
        assert!(!live_child_paths("alice").is_empty(), "one child still live");
        cmd_worker_stop("alice-w2");
        assert!(!has_live_children("alice"));
    }
}

-        assert!(!has_live_children("alice"));

+        assert!(live_child_paths("alice").is_empty());


//! recursively deleting in-flight work. [`has_live_children`] /
//! [`live_child_paths`] are the guard primitives T7's `session-end` consults.

//! recursively deleting in-flight work. [`live_child_paths`] is the guard
//! primitive T7's `session-end` consults.


//! recursively deleting in-flight work. [`live_child_paths`] is the guard
//! primitive T7's `session-end` consults.

//! Worker perches: `api worker-start` / `worker-stop` / `worker-poll`, plus the
//! **cascade-wipe guard** (KNOWN-HAZARDS 6.3).
//!
//! A worker is a nested child perch under a parent agent
//! (`owlery/<parent>/nested/<worker>/`), addressable like any perch and routed
//! through the *single* path resolver (REQ-HAZARD-SINGLE-PATH-SOURCE / WORKER-
//! PATH — no divergent sibling layouts).
//!
//! ## Cascade-wipe guard (6.3, REQ-HAZARD-CASCADE-WIPE-GUARD)
//! A destructive parent teardown must never hard-delete a parent that still
//! hosts **live** children. "Live" here = the child still carries its `ready`
//! marker (the soft-cleanup signal; `worker-stop` removes it). When live
//! children exist, the caller soft-cleans and surfaces their paths instead of
//! recursively deleting in-flight work. [`has_live_children`] /
//! [`live_child_paths`] are the guard primitives T7's `session-end` consults.
//
// [impl->REQ-API-2]
// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]

use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;

use super::auth;
use super::EXIT_REFUSED;

/// `api worker-start <parent> <id>` — create a nested worker perch under
/// `parent`. Resolves with an Explicit parent so the layout is unambiguous even
/// if the id's own suffix would infer a different one.
// [impl->REQ-API-2]
pub fn cmd_worker_start(parent: &str, id: &str) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Explicit(parent));
    if let Err(e) = std::fs::create_dir_all(&perch_path) {
        eprintln!("WORKER_START_FAIL:{id}: {e}");
        return EXIT_REFUSED;
    }
    let rec = InfoJson::new(id, &now_stamp(), std::process::id(), "", "worker");
    if let Err(e) = info::write_info(&perch_path, &rec) {
        eprintln!("WORKER_START_FAIL:{id}: {e}");
        return EXIT_REFUSED;
    }
    // ready marker = the live-child signal the cascade-wipe guard checks.
    let _ = std::fs::write(
        perch::resolve_ready_file(id, ParentHint::Explicit(parent)),
        "",
    );
    let token = auth::mint_token();
    let _ = auth::write_token(&perch_path, &token);
    eprintln!("WORKER_STARTED:{id} under {parent} token={token}");
    0
}

/// `api worker-stop <id>` — soft-clean a worker (REQ-HAZARD-SOFT-CLEANUP):
/// remove its `ready` marker so it no longer counts as a live child; the
/// `info.json` + spool are preserved. Idempotent.
// [impl->REQ-API-2]
pub fn cmd_worker_stop(id: &str) -> i32 {
    let removed = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer)).is_ok();
    if removed {
        eprintln!("WORKER_STOPPED:{id}");
    } else {
        eprintln!("WORKER_STOPPED:{id} (no ready marker)");
    }
    0
}

/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), framed
/// to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    for msg in drained.unwrap_or_default() {
        use std::io::Write;
        print!("{msg}");
        let _ = std::io::stdout().flush();
    }
    0
}

/// The paths of `parent_id`'s **live** nested children (those still carrying a
/// `ready` marker). Empty ⇒ the parent is safe to hard-delete.
// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]
pub fn live_child_paths(parent_id: &str) -> Vec<std::path::PathBuf> {
    let nested = perch::resolve_perch_path(parent_id, ParentHint::Infer).join("nested");
    let mut live = Vec::new();
    let Ok(entries) = std::fs::read_dir(&nested) else {
        return live;
    };
    for entry in entries.flatten() {
        let child = entry.path();
        if !child.is_dir() {
            continue;
        }
        // A child is live while its ready marker exists (info.json present =
        // it's a real perch; ready = not yet soft-cleaned).
        if info::read_info(&child).is_some() && child.join("ready").exists() {
            live.push(child);
        }
    }
    live
}

fn now_stamp() -> String {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".to_string())
}

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

    fn establish_parent(id: &str) {
        let path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&path).unwrap();
        info::write_info(
            &path,
            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),
        )
        .unwrap();
    }

    #[test] // [unit->REQ-API-2] a worker nests under its parent and is addressable.
    fn worker_start_nests_under_parent() {
        let _h = isolated_home();
        establish_parent("alice");
        assert_eq!(cmd_worker_start("alice", "alice-w1"), 0);

        let expected = perch::resolve_perch_path("alice", ParentHint::Infer)
            .join("nested")
            .join("alice-w1");
        assert!(expected.join("info.json").exists());
        assert!(expected.join("ready").exists());
        // Inferred resolution agrees with the explicit nesting.
        assert_eq!(perch::resolve_perch_path("alice-w1", ParentHint::Infer), expected);
    }

    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] a parent with a live worker
    // reports children; stopping the worker clears the guard.
    fn cascade_guard_tracks_live_children() {
        let _h = isolated_home();
        establish_parent("alice");
        assert!(live_child_paths("alice").is_empty(), "no children yet");

        cmd_worker_start("alice", "alice-w1");
        assert!(!live_child_paths("alice").is_empty(), "live worker must be seen");
        assert_eq!(live_child_paths("alice").len(), 1);

        // Soft-stop the worker → ready marker gone → no longer a live child.
        assert_eq!(cmd_worker_stop("alice-w1"), 0);
        assert!(live_child_paths("alice").is_empty(), "soft-stopped worker is not live");
        // The record is preserved (soft cleanup), only the ready marker removed.
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        assert!(child.join("info.json").exists());
        assert!(!child.join("ready").exists());
    }

    #[test] // [unit->REQ-API-2] worker-poll drains the worker's own spool.
    fn worker_poll_drains_spool() {
        let _h = isolated_home();
        establish_parent("alice");
        cmd_worker_start("alice", "alice-w1");
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        spool::spool_message_at(&child, "alice", "task result").unwrap();

        let perch_path = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        let got = spool::drain_non_deferred_at(&perch_path).unwrap();
        assert_eq!(got.len(), 1);
        // And the command path returns 0.
        cmd_worker_start("alice", "alice-w2");
        assert_eq!(cmd_worker_poll("alice-w2", false), 0);
    }

    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] two workers: guard holds until
    // the last live child is stopped.
    fn guard_clears_only_when_all_children_stopped() {
        let _h = isolated_home();
        establish_parent("alice");
        cmd_worker_start("alice", "alice-w1");
        cmd_worker_start("alice", "alice-w2");
        assert_eq!(live_child_paths("alice").len(), 2);
        cmd_worker_stop("alice-w1");
        assert!(!live_child_paths("alice").is_empty(), "one child still live");
        cmd_worker_stop("alice-w2");
        assert!(live_child_paths("alice").is_empty());
    }
}

-//! recursively deleting in-flight work. [`has_live_children`] /

-//! [`live_child_paths`] are the guard primitives T7's `session-end` consults.

+//! recursively deleting in-flight work. [`live_child_paths`] is the guard

+//! primitive T7's `session-end` consults.


Now `allow(dead_code)` the deliberate forward/reader APIs (M2b consumes them).


/// Whether the idle / echo-gate sentinels are currently set (readers/tests).
pub fn is_idle(id: &str) -> bool {
    sentinel_path(id, IDLE_SENTINEL).exists()
}
pub fn echo_gate_armed(id: &str) -> bool {
    sentinel_path(id, ECHO_GATE_SENTINEL).exists()
}

/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).
pub fn is_idle(id: &str) -> bool {
    sentinel_path(id, IDLE_SENTINEL).exists()
}
/// Whether the echo-gate is armed. A reader for the M2b echo-commune trigger;
/// exercised by the activity tests today.
#[allow(dead_code)] // consumed by the M2b echo-commune path
pub fn echo_gate_armed(id: &str) -> bool {
    sentinel_path(id, ECHO_GATE_SENTINEL).exists()
}


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

/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).
pub fn is_idle(id: &str) -> bool {
    sentinel_path(id, IDLE_SENTINEL).exists()
}
/// Whether the echo-gate is armed. A reader for the M2b echo-commune trigger;
/// exercised by the activity tests today.
#[allow(dead_code)] // consumed by the M2b echo-commune path
pub fn echo_gate_armed(id: &str) -> bool {
    sentinel_path(id, ECHO_GATE_SENTINEL).exists()
}

//! Delivery + activity: `api state`, `api echo-gate`, `api poll`, and
//! inject-method selection.
//!
//! Activity state lives in the perch as **spt-core-owned sentinels** (CONTEXT
//! §activity/idle detection) — the adapter only *calls* the commands; it never
//! names or writes the sentinels (MANIFEST §What is NOT in the manifest):
//! - `.idle`      — the activity/idle sentinel (`api state busy|idle`).
//! - `.more-done` — the echo-commune gate (armed by `api state idle` unless
//!   `--no-gate`; togglable directly via `api echo-gate set|clear`). This is the
//!   `can_inject = false` fallback path: a hook that can't inject context arms
//!   the gate so a later relay/poll surfaces it instead (CONTEXT §hook output).
//!
//! `api poll` is the **short-lived hook-channel drain** (distinct from
//! `api listen`'s long-running relay): it drains the spool and prints each
//! message framed to stdout. The inject *method* (hook/relay; PTY is M3) is
//! resolved from the manifest's `[inject]` table.
//
// [impl->REQ-SEAM-ACTIVITY]
// [impl->REQ-SEAM-INJECT]
// [impl->REQ-API-2]

use spt_runtime::manifest::InjectMethod;
use spt_runtime::Manifest;
use spt_store::perch::{self, ParentHint};
use spt_store::spool;

use super::EXIT_REFUSED;

/// The activity/idle sentinel filename within a perch dir.
const IDLE_SENTINEL: &str = ".idle";
/// The echo-commune gate sentinel (`.more-done`-equiv; modern spt couples it to
/// the idle transition).
const ECHO_GATE_SENTINEL: &str = ".more-done";

fn sentinel_path(id: &str, name: &str) -> std::path::PathBuf {
    perch::resolve_perch_path(id, ParentHint::Infer).join(name)
}

/// Set/clear the `.idle` sentinel for an activity transition. Idle additionally
/// arms the echo-gate unless `no_gate` (CONTEXT §api state).
// [impl->REQ-SEAM-ACTIVITY]
pub fn cmd_state(id: &str, state: &str, no_gate: bool) -> i32 {
    let idle = sentinel_path(id, IDLE_SENTINEL);
    match state {
        "idle" => {
            if let Err(e) = std::fs::write(&idle, "") {
                eprintln!("STATE_FAIL:{id}: {e}");
                return EXIT_REFUSED;
            }
            if !no_gate {
                let _ = std::fs::write(sentinel_path(id, ECHO_GATE_SENTINEL), "");
            }
            eprintln!("IDLE:{id}{}", if no_gate { " (no-gate)" } else { " (gate-armed)" });
            0
        }
        "busy" => {
            // Removing a non-existent sentinel is fine (idempotent transition).
            let _ = std::fs::remove_file(&idle);
            eprintln!("BUSY:{id}");
            0
        }
        other => {
            eprintln!("USAGE: state must be busy|idle, got {other}");
            EXIT_REFUSED
        }
    }
}

/// `api echo-gate set|clear` — explicit control of the echo-commune gate,
/// independent of idle (CONTEXT §api state).
// [impl->REQ-SEAM-ACTIVITY]
pub fn cmd_echo_gate(id: &str, action: &str) -> i32 {
    let gate = sentinel_path(id, ECHO_GATE_SENTINEL);
    match action {
        "set" => {
            if let Err(e) = std::fs::write(&gate, "") {
                eprintln!("ECHO_GATE_FAIL:{id}: {e}");
                return EXIT_REFUSED;
            }
            eprintln!("ECHO_GATE_SET:{id}");
            0
        }
        "clear" => {
            let _ = std::fs::remove_file(&gate);
            eprintln!("ECHO_GATE_CLEAR:{id}");
            0
        }
        other => {
            eprintln!("USAGE: echo-gate must be set|clear, got {other}");
            EXIT_REFUSED
        }
    }
}

/// Whether the idle / echo-gate sentinels are currently set (readers/tests).
pub fn is_idle(id: &str) -> bool {
    sentinel_path(id, IDLE_SENTINEL).exists()
}
pub fn echo_gate_armed(id: &str) -> bool {
    sentinel_path(id, ECHO_GATE_SENTINEL).exists()
}

/// Resolve the inject methods for an activity state from the manifest's
/// `[inject]` table, **dropping `pty`** (PTY injection is M3 — `spt-term`). With
/// no manifest or no configured methods, default to `hook` (the universally-
/// available channel). REQ-SEAM-INJECT.
// [impl->REQ-SEAM-INJECT]
pub fn resolve_inject_methods(manifest: Option<&Manifest>, idle: bool) -> Vec<InjectMethod> {
    let configured = manifest.and_then(|m| m.inject.as_ref()).map(|inj| {
        if idle { &inj.idle } else { &inj.activity }
    });
    let mut methods: Vec<InjectMethod> = configured
        .map(|v| {
            v.iter()
                .copied()
                .filter(|m| *m != InjectMethod::Pty) // PTY = M3
                .collect()
        })
        .unwrap_or_default();
    if methods.is_empty() {
        methods.push(InjectMethod::Hook);
    }
    methods
}

/// `api poll <id>` — drain the spool (hook-channel delivery) and print each
/// message framed to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {
    // Inject-method selection is resolved (hook/relay) even though the M2a poll
    // channel is the hook drain; PTY delivery lands with the broker in M3.
    let _methods = resolve_inject_methods(manifest, is_idle(id));
    for msg in poll_drain(id, include_deferred) {
        emit(&msg);
    }
    0
}

/// Drain the spool for `id`, returning the messages (oldest first). Separated
/// from `cmd_poll` so it is unit-testable without capturing stdout.
pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<String> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    drained.unwrap_or_default()
}

fn emit(msg: &str) {
    use std::io::Write;
    print!("{msg}");
    let _ = std::io::stdout().flush();
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::isolated_home;
    use spt_store::info::{self, InfoJson};

    fn establish(id: &str) {
        let path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&path).unwrap();
        info::write_info(
            &path,
            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),
        )
        .unwrap();
    }

    #[test] // [unit->REQ-SEAM-ACTIVITY] idle writes .idle + arms the gate; busy clears .idle.
    fn idle_busy_transitions_manage_sentinels() {
        let _h = isolated_home();
        establish("alice");
        assert_eq!(cmd_state("alice", "idle", false), 0);
        assert!(is_idle("alice"));
        assert!(echo_gate_armed("alice"), "idle arms the echo-gate by default");

        assert_eq!(cmd_state("alice", "busy", false), 0);
        assert!(!is_idle("alice"));
        // The gate persists across busy (cleared explicitly / on echo fire).
        assert!(echo_gate_armed("alice"));
    }

    #[test] // [unit->REQ-SEAM-ACTIVITY] --no-gate suppresses the idle↔gate coupling.
    fn idle_no_gate_does_not_arm_gate() {
        let _h = isolated_home();
        establish("alice");
        assert_eq!(cmd_state("alice", "idle", true), 0);
        assert!(is_idle("alice"));
        assert!(!echo_gate_armed("alice"));
    }

    #[test] // [unit->REQ-SEAM-ACTIVITY] echo-gate set/clear is independent of idle.
    fn echo_gate_explicit_toggle() {
        let _h = isolated_home();
        establish("alice");
        assert_eq!(cmd_echo_gate("alice", "set"), 0);
        assert!(echo_gate_armed("alice"));
        assert_eq!(cmd_echo_gate("alice", "clear"), 0);
        assert!(!echo_gate_armed("alice"));
        assert!(!is_idle("alice"), "echo-gate must not touch the idle sentinel");
    }

    #[test] // [unit->REQ-API-2] poll drains spooled messages; deferred only with the flag.
    fn poll_drains_non_deferred_by_default() {
        let _h = isolated_home();
        establish("alice");
        let perch_path = perch::resolve_perch_path("alice", ParentHint::Infer);
        spool::spool_message_at(&perch_path, "bob", "live one").unwrap();
        spool::spool_message_deferred_at(&perch_path, "bob", "deferred one").unwrap();

        let got = poll_drain("alice", false);
        assert_eq!(got.len(), 1, "deferred row must not drain without the flag");
        assert!(got[0].contains("live one"));

        // The deferred row survives and drains only with the flag.
        let got2 = poll_drain("alice", true);
        assert_eq!(got2.len(), 1);
        assert!(got2[0].contains("deferred one"));
    }

    #[test] // [unit->REQ-SEAM-INJECT] inject resolution drops PTY (M3) and defaults to hook.
    fn inject_resolution_drops_pty_defaults_hook() {
        // No manifest → default hook.
        assert_eq!(resolve_inject_methods(None, true), vec![InjectMethod::Hook]);

        let m = Manifest::from_toml_str(
            "[adapter]\nname=\"m\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n[inject]\nidle=[\"pty\",\"hook\"]\nactivity=[\"hook\"]\n",
        )
        .unwrap();
        // idle drops pty, keeps hook.
        assert_eq!(resolve_inject_methods(Some(&m), true), vec![InjectMethod::Hook]);
        assert_eq!(resolve_inject_methods(Some(&m), false), vec![InjectMethod::Hook]);
    }

    #[test] // [unit->REQ-SEAM-INJECT] an all-pty config still yields a usable channel.
    fn inject_all_pty_falls_back_to_hook() {
        let m = Manifest::from_toml_str(
            "[adapter]\nname=\"m\"\nversion=\"1\"\nmin_spt_core_version=\"1\"\n\n[inject]\nidle=[\"pty\"]\n",
        )
        .unwrap();
        assert_eq!(resolve_inject_methods(Some(&m), true), vec![InjectMethod::Hook]);
    }
}

-/// Whether the idle / echo-gate sentinels are currently set (readers/tests).

+/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).

+/// Whether the echo-gate is armed. A reader for the M2b echo-commune trigger;

+/// exercised by the activity tests today.

+#[allow(dead_code)] // consumed by the M2b echo-commune path


/// M2a resolves them; M2b ingests them.
// [impl->REQ-API-3]
pub fn resolve_filedrops(manifest: Option<&Manifest>, id: &str) -> (Option<PathBuf>, Option<PathBuf>) {

/// M2a resolves them; M2b ingests them.
// [impl->REQ-API-3]
#[allow(dead_code)] // M2a declares the file-drop contract; M2b's ingest consumes it
pub fn resolve_filedrops(manifest: Option<&Manifest>, id: &str) -> (Option<PathBuf>, Option<PathBuf>) {


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

/// M2a resolves them; M2b ingests them.
// [impl->REQ-API-3]
pub fn resolve_filedrops(manifest: Option<&Manifest>, id: &str) -> (Option<PathBuf>, Option<PathBuf>) {

/// M2a resolves them; M2b ingests them.
// [impl->REQ-API-3]
#[allow(dead_code)] // M2a declares the file-drop contract; M2b's ingest consumes it
pub fn resolve_filedrops(manifest: Option<&Manifest>, id: &str) -> (Option<PathBuf>, Option<PathBuf>) {

/// dirs; the filename is spt-core-fixed (`<id>-commune.md` / `<id>-signoff.md`).

+#[allow(dead_code)] // M2a declares the file-drop contract; M2b's ingest consumes it

let session = manifest.map(|m| &m.session);


/// session (REQ-START-4). Only `inject` directives with a value participate.
// [impl->REQ-START-4]
pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {

/// session (REQ-START-4). Only `inject` directives with a value participate.
// [impl->REQ-START-4]
#[allow(dead_code)] // M2a resolves the alias injections; the spawn-env wiring is M2b
pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {


<tool_use_error>String to replace not found in file.
String: /// session (REQ-START-4). Only `inject` directives with a value participate.
// [impl->REQ-START-4]
pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {</tool_use_error>

Error: String to replace not found in file.
String: /// session (REQ-START-4). Only `inject` directives with a value participate.
// [impl->REQ-START-4]
pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && grep -n "pub fn env_injections" -B1 crates/spt/src/api/reporting.rs

Locate env_injections signature


183-// [impl->REQ-START-4]
184:pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {


Edit

// [impl->REQ-START-4]
pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {

// [impl->REQ-START-4]
#[allow(dead_code)] // M2a resolves the alias injections; the spawn-env wiring is M2b
pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {


// [impl->REQ-START-4]
pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {

// [impl->REQ-START-4]
#[allow(dead_code)] // M2a resolves the alias injections; the spawn-env wiring is M2b
pub fn env_injections(manifest: Option<&Manifest>) -> Vec<(String, String)> {

/// channel through which the adapter's `$SPT`/`$OWL`/`$LIVE` aliases reach a

/// session. Only `inject` directives with a value participate (REQ-START-4).

+#[allow(dead_code)] // M2a resolves the alias injections; the spawn-env wiring is M2b

let Some(m) = manifest else { return Vec::new() };


T0: Scaffold spt-runtime crate

New crate (members + deps spt-proto/store/msg, serde, toml); activation-prep. Compiles; traceable-reqs check green.

T1: Manifest schema + parse + validate

Typed structs for every docs/MANIFEST.md section + parse + validate (helpful errors). REQ-MANIFEST-1.

T2: AgentRuntime trait + ManifestRuntime

Substitution-key catalog + opaque-template fill + spawn-session seam (timeout). REQ-SEAM-SPAWN, REQ-HAZARD-SUBPROCESS-TIMEOUT.

T3: spt api dispatch group + local-api-auth

--manifest/--adapter resolution, adapter_name on every call, exit codes, local-api-auth guard. REQ-API-1, REQ-HAZARD-LOCAL-API-AUTH.

T4: Harness-hosted startup (seed/listen/bind)

api seed (interim seed-file by parent_pid) + api listen (consume seed, validate session_id, bind skeleton->live, relay loop) + api bind. REQ-START-1/2, REQ-SEAM-POSTSPAWN, REQ-API-2, REQ-HAZARD-WINDOWS-PID-RECYCLE.

T5: Delivery + activity (state/poll/inject)

api state busy|idle (+echo-gate sentinel, --no-gate, echo-gate set/clear) + api poll [--include-deferred] + inject-method selection. REQ-SEAM-ACTIVITY, REQ-SEAM-INJECT, REQ-API-2.

T6: Worker perches + cascade-wipe guard

api worker-start/worker-stop/worker-poll + cascade-wipe guard. REQ-API-2, REQ-HAZARD-CASCADE-WIPE-GUARD.

T7: Reporting + boundary surface

api boundary clear|compact + session-end (--erase) + presence + history-log + emit stub + capability hostable_types + env-alias supply. REQ-API-2/3, REQ-START-4, REQ-SEAM-CAPABILITY, REQ-HAZARD-SOFT-CLEANUP.

T8: Generic mock adapter

adapters/mock/manifest.toml + cross-platform session scripts/helper bin; zero CC conventions. REQ-MANIFEST-1, REQ-API-3.

T9: Contract E2E test

load mock manifest -> ManifestRuntime spawns session -> api bind -> api state idle -> api poll delivers spooled message. REQ-API-1/2 int, REQ-START-2 int.

T10: Activation sweep

Activate M2a reqs + hazards + REQ-ARCH-2, green check, CI green, amend ROADMAP/CONTEXT for split.


Run clippy `-D warnings` to confirm clean, plus the E2E.


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -15

Clippy deny-warnings gate


    |                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace the closure with the function itself: `reporting::cmd_presence`
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#redundant_closure

error: redundant closure
   --> crates\spt\src\api\mod.rs:239:62
    |
239 |         ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, |id| reporting::cmd_history_log(id)),
    |                                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace the closure with the function itself: `reporting::cmd_history_log`
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#redundant_closure

error: could not compile `spt` (bin "spt") due to 3 previous errors
warning: build failed, waiting for other jobs to finish...
error: could not compile `spt` (bin "spt" test) due to 3 previous errors


Clippy: redundant closures. Replace passthrough closures with the fn path.


Edit

ApiCmd::WorkerStop { id, auth } => gated(&id, &auth, |id| worker::cmd_worker_stop(id)),

ApiCmd::WorkerStop { id, auth } => gated(&id, &auth, worker::cmd_worker_stop),


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

ApiCmd::WorkerStart { parent, id, auth } => {

gated(&parent, &auth, |parent| worker::cmd_worker_start(parent, &id))

-        ApiCmd::WorkerStop { id, auth } => gated(&id, &auth, |id| worker::cmd_worker_stop(id)),

+        ApiCmd::WorkerStop { id, auth } => gated(&id, &auth, worker::cmd_worker_stop),

gated(&id, &auth, |id| worker::cmd_worker_poll(id, false))


        ApiCmd::Presence { id, auth } => gated(&id, &auth, |id| reporting::cmd_presence(id)),
        ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, |id| reporting::cmd_history_log(id)),

        ApiCmd::Presence { id, auth } => gated(&id, &auth, reporting::cmd_presence),
        ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, reporting::cmd_history_log),


        ApiCmd::Presence { id, auth } => gated(&id, &auth, |id| reporting::cmd_presence(id)),
        ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, |id| reporting::cmd_history_log(id)),

        ApiCmd::Presence { id, auth } => gated(&id, &auth, reporting::cmd_presence),
        ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, reporting::cmd_history_log),

ApiCmd::SessionEnd { id, erase, auth } => {

gated(&id, &auth, |id| reporting::cmd_session_end(id, erase))

-        ApiCmd::Presence { id, auth } => gated(&id, &auth, |id| reporting::cmd_presence(id)),

-        ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, |id| reporting::cmd_history_log(id)),

+        ApiCmd::Presence { id, auth } => gated(&id, &auth, reporting::cmd_presence),

+        ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, reporting::cmd_history_log),

gated(&id, &auth, |id| reporting::cmd_emit(id, &r#type))


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -8

Re-run clippy gate


    Checking spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.56s


Clippy clean. Run E2E + workspace, then activate T9 int reqs.


cd "C:\Users\decid\Documents\projects\spt-core" && cargo test --workspace 2>&1 | grep -E "test result: FAIL|error|contract_e2e|passed" | grep -v "0 failed" | head; echo "---e2e---"; cargo test -p spt --test contract_e2e 2>&1 | grep "test result"

Workspace + E2E test


test cli::tests::invalid_usage_errors ... ok
     Running tests\contract_e2e.rs (target\debug\deps\contract_e2e-0e9cfdca991b371f.exe)
test identity::tests::bad_inputs_error_without_panic ... ok
test manifest::tests::syntactically_broken_toml_is_an_error_not_a_panic ... ok
test runtime::tests::missing_key_errors_before_spawn ... ok
test manifest::tests::unknown_enum_variant_is_an_error_not_a_panic ... ok
test runtime::tests::unknown_role_errors ... ok
test registry::tests::resolve_on_empty_registry_is_none_not_error ... ok
---e2e---
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s


cd "C:\Users\decid\Documents\projects\spt-core" && cargo test --workspace 2>&1 | grep -c "test result: ok"; cargo test --workspace 2>&1 | grep "FAILED\|test result: FAILED" | head

Confirm no failures workspace-wide


All green. Activate T9 int evidence: REQ-API-1, REQ-API-2, REQ-START-2.


Edit

id = "REQ-API-1"
title = "api prefix and adapter_name on every machinery invocation"
required_stages = ["impl", "unit"]

id = "REQ-API-1"
title = "api prefix and adapter_name on every machinery invocation"
required_stages = ["impl", "unit", "int"]


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-API-1"
title = "api prefix and adapter_name on every machinery invocation"
required_stages = ["impl", "unit"]

id = "REQ-API-1"
title = "api prefix and adapter_name on every machinery invocation"
required_stages = ["impl", "unit", "int"]

+required_stages = ["impl", "unit", "int"]

title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"


id = "REQ-API-2"
title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
required_stages = []

id = "REQ-API-2"
title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
required_stages = ["impl", "unit", "int"]


id = "REQ-API-2"
title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
required_stages = []

id = "REQ-API-2"
title = "The api subcommand surface (bind/listen/poll/state/worker/boundary/...)"
required_stages = ["impl", "unit", "int"]

title = "commune/signoff are file-drops, not commands"


id = "REQ-START-2"
title = "Harness-hosted startup: api seed then listen"
required_stages = ["impl", "unit"]

id = "REQ-START-2"
title = "Harness-hosted startup: api seed then listen"
required_stages = ["impl", "unit", "int"]


id = "REQ-START-2"
title = "Harness-hosted startup: api seed then listen"
required_stages = ["impl", "unit"]

id = "REQ-START-2"
title = "Harness-hosted startup: api seed then listen"
required_stages = ["impl", "unit", "int"]

title = "spt-hosted startup: spawn-session then api bind (no file)"


T0: Scaffold spt-runtime crate

T1: Manifest schema + parse + validate

T6: Worker perches + cascade-wipe guard

T7: Reporting + boundary surface

T8: Generic mock adapter

T9: Contract E2E test

T10: Activation sweep


The E2E uses `bind` (post-spawn); REQ-START-2 is seed→listen. Add a seed→listen→deliver test to faithfully provide that int evidence.


//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`
//! helper → the helper fires `spt api bind` (skeleton→live) then
//! `spt api state idle` → a message is spooled to the perch → `spt api poll`
//! drains it. Asserts the round-trip.
//
// [int->REQ-API-1]
// [int->REQ-API-2]
// [int->REQ-START-2]

//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`
//! helper → the helper fires `spt api bind` (skeleton→live) then
//! `spt api state idle` → a message is spooled to the perch → `spt api poll`
//! drains it. Asserts the round-trip.
//!
//! A second test exercises the **harness-hosted startup** topology proper —
//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered
//! message — the REQ-START-2 path.
//
// [int->REQ-API-1]
// [int->REQ-API-2]


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

//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`
//! helper → the helper fires `spt api bind` (skeleton→live) then
//! `spt api state idle` → a message is spooled to the perch → `spt api poll`
//! drains it. Asserts the round-trip.
//!
//! A second test exercises the **harness-hosted startup** topology proper —
//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered
//! message — the REQ-START-2 path.
//
// [int->REQ-API-1]
// [int->REQ-API-2]

//! M2a contract E2E (T9): the inbound + outbound halves of the harness contract
//! proven end-to-end through the **generic mock adapter** — no vendor harness.
//!
//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`
//! helper → the helper fires `spt api bind` (skeleton→live) then
//! `spt api state idle` → a message is spooled to the perch → `spt api poll`
//! drains it. Asserts the round-trip.
//
// [int->REQ-API-1]
// [int->REQ-API-2]
// [int->REQ-START-2]

use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;

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

/// Absolute path to a sibling binary in the same target dir as the `spt` test bin.
fn sibling_bin(name: &str) -> PathBuf {
    let spt = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    spt.with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

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

#[test]
fn mock_adapter_drives_the_full_contract() {
    // Isolated home shared by this process *and* the child `spt` invocations
    // (inherited via env). This test owns the process, so a plain set_var is safe.
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at
    // the freshly-built helper + spt binary (PATH isn't set up in the test rig).
    let manifest_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../adapters/mock/manifest.toml");
    let mut manifest = Manifest::from_toml_str(
        &std::fs::read_to_string(manifest_path).expect("read mock manifest"),
    )
    .expect("mock manifest valid");
    manifest.session.self_.as_mut().unwrap().command = format!(
        "\"{}\" --id {{id}} --session-id {{session_id}} --spt \"{}\"",
        mock_bin.display(),
        spt_bin.display(),
    );

    let runtime = ManifestRuntime::new(manifest);
    let id = "mock-e2e";
    let session_id = "sid-e2e-1";

    // Outbound: spawn the session. Bounded (not detached) so the test waits for
    // the helper's bind + state-idle to complete before asserting.
    let out = runtime
        .run_bounded("self", &keys(&[("id", id), ("session_id", session_id)]), Duration::from_secs(30))
        .expect("spawn mock session");
    assert!(
        out.success(),
        "mock-session failed: code={:?}\nstdout={}\nstderr={}",
        out.status_code, out.stdout, out.stderr
    );

    // Inbound assertion 1 — `api bind` flipped the perch live (REQ-START-2 /
    // REQ-SEAM-POSTSPAWN): info.json exists with the reported session id.
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after bind");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");

    // Inbound assertion 2 — `api state idle` wrote the activity sentinel.
    assert!(perch_path.join(".idle").exists(), "state idle must write the .idle sentinel");

    // A peer spools a message to the now-live perch.
    spool::spool_message_at(&perch_path, "tester", "hello over the contract").unwrap();

    // Inbound assertion 3 — `api poll` drains it (the full delivery round-trip).
    let poll = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api poll");
    assert!(poll.status.success(), "api poll exit: {:?}", poll.status);
    let stdout = String::from_utf8_lossy(&poll.stdout);
    assert!(
        stdout.contains("hello over the contract"),
        "api poll must deliver the spooled message, got: {stdout:?}"
    );

    // Auth negative — a poll with no proof is refused (REQ-HAZARD-LOCAL-API-AUTH
    // exercised through the real binary).
    let unauth = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id])
        .env("SPT_HOME", home.path())
        .env_remove("OWL_SESSION_ID")
        .output()
        .expect("run unauth poll");
    assert!(!unauth.status.success(), "poll without proof must be refused");
}

+//! A second test exercises the **harness-hosted startup** topology proper —

+//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered


    assert!(!unauth.status.success(), "poll without proof must be refused");
}

    assert!(!unauth.status.success(), "poll without proof must be refused");
}

/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed
/// (matched by parent pid, validated against pid-recycle), binds the perch
/// skeleton→live, and relays a delivered message.
#[test]
fn seed_then_listen_binds_and_relays() {
    // [int->REQ-START-2]
    // [int->REQ-API-2]
    let home = tempfile::tempdir().unwrap();
    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let id = "listen-e2e";
    let session_id = "sid-listen-1";
    // The anchor must be a live process; the test runner itself qualifies.
    let anchor = std::process::id().to_string();

    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();

    // Seed the startup record keyed by the anchor pid.
    let seed = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .status()
        .expect("run api seed");
    assert!(seed.success(), "api seed exit: {seed:?}");

    // Listen consumes the seed (explicit --parent-pid for a deterministic match),
    // binds the perch, drains the backlog, and exits (--once).
    let listen = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api listen");
    assert!(listen.status.success(), "api listen exit: {:?}", listen.status);
    let stdout = String::from_utf8_lossy(&listen.stdout);
    assert!(
        stdout.contains("delivered via listen"),
        "listen must relay the spooled message, got: {stdout:?}"
    );

    // The perch was bound live with the seed's session id.
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after listen");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");
}


    assert!(!unauth.status.success(), "poll without proof must be refused");
}

/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed
/// (matched by parent pid, validated against pid-recycle), binds the perch
/// skeleton→live, and relays a delivered message.
#[test]
fn seed_then_listen_binds_and_relays() {
    // [int->REQ-START-2]
    // [int->REQ-API-2]
    let home = tempfile::tempdir().unwrap();
    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let id = "listen-e2e";
    let session_id = "sid-listen-1";
    // The anchor must be a live process; the test runner itself qualifies.
    let anchor = std::process::id().to_string();

    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();

    // Seed the startup record keyed by the anchor pid.
    let seed = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .status()
        .expect("run api seed");
    assert!(seed.success(), "api seed exit: {seed:?}");

    // Listen consumes the seed (explicit --parent-pid for a deterministic match),
    // binds the perch, drains the backlog, and exits (--once).
    let listen = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api listen");
    assert!(listen.status.success(), "api listen exit: {:?}", listen.status);
    let stdout = String::from_utf8_lossy(&listen.stdout);
    assert!(
        stdout.contains("delivered via listen"),
        "listen must relay the spooled message, got: {stdout:?}"
    );

    // The perch was bound live with the seed's session id.
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after listen");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");
}

//! M2a contract E2E (T9): the inbound + outbound halves of the harness contract
//! proven end-to-end through the **generic mock adapter** — no vendor harness.
//!
//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`
//! helper → the helper fires `spt api bind` (skeleton→live) then
//! `spt api state idle` → a message is spooled to the perch → `spt api poll`
//! drains it. Asserts the round-trip.
//!
//! A second test exercises the **harness-hosted startup** topology proper —
//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered
//! message — the REQ-START-2 path.
//
// [int->REQ-API-1]
// [int->REQ-API-2]

use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;

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

/// Absolute path to a sibling binary in the same target dir as the `spt` test bin.
fn sibling_bin(name: &str) -> PathBuf {
    let spt = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    spt.with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

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

#[test]
fn mock_adapter_drives_the_full_contract() {
    // Isolated home shared by this process *and* the child `spt` invocations
    // (inherited via env). This test owns the process, so a plain set_var is safe.
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at
    // the freshly-built helper + spt binary (PATH isn't set up in the test rig).
    let manifest_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../adapters/mock/manifest.toml");
    let mut manifest = Manifest::from_toml_str(
        &std::fs::read_to_string(manifest_path).expect("read mock manifest"),
    )
    .expect("mock manifest valid");
    manifest.session.self_.as_mut().unwrap().command = format!(
        "\"{}\" --id {{id}} --session-id {{session_id}} --spt \"{}\"",
        mock_bin.display(),
        spt_bin.display(),
    );

    let runtime = ManifestRuntime::new(manifest);
    let id = "mock-e2e";
    let session_id = "sid-e2e-1";

    // Outbound: spawn the session. Bounded (not detached) so the test waits for
    // the helper's bind + state-idle to complete before asserting.
    let out = runtime
        .run_bounded("self", &keys(&[("id", id), ("session_id", session_id)]), Duration::from_secs(30))
        .expect("spawn mock session");
    assert!(
        out.success(),
        "mock-session failed: code={:?}\nstdout={}\nstderr={}",
        out.status_code, out.stdout, out.stderr
    );

    // Inbound assertion 1 — `api bind` flipped the perch live (REQ-START-2 /
    // REQ-SEAM-POSTSPAWN): info.json exists with the reported session id.
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after bind");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");

    // Inbound assertion 2 — `api state idle` wrote the activity sentinel.
    assert!(perch_path.join(".idle").exists(), "state idle must write the .idle sentinel");

    // A peer spools a message to the now-live perch.
    spool::spool_message_at(&perch_path, "tester", "hello over the contract").unwrap();

    // Inbound assertion 3 — `api poll` drains it (the full delivery round-trip).
    let poll = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api poll");
    assert!(poll.status.success(), "api poll exit: {:?}", poll.status);
    let stdout = String::from_utf8_lossy(&poll.stdout);
    assert!(
        stdout.contains("hello over the contract"),
        "api poll must deliver the spooled message, got: {stdout:?}"
    );

    // Auth negative — a poll with no proof is refused (REQ-HAZARD-LOCAL-API-AUTH
    // exercised through the real binary).
    let unauth = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id])
        .env("SPT_HOME", home.path())
        .env_remove("OWL_SESSION_ID")
        .output()
        .expect("run unauth poll");
    assert!(!unauth.status.success(), "poll without proof must be refused");
}

+/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed

+/// (matched by parent pid, validated against pid-recycle), binds the perch

+/// skeleton→live, and relays a delivered message.

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

+    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));

+    // The anchor must be a live process; the test runner itself qualifies.

+    let anchor = std::process::id().to_string();

+    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.

+    std::env::set_var("SPT_HOME", home.path());

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

+    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();

+    // Seed the startup record keyed by the anchor pid.

+        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])

+    assert!(seed.success(), "api seed exit: {seed:?}");

+    // Listen consumes the seed (explicit --parent-pid for a deterministic match),

+    // binds the perch, drains the backlog, and exits (--once).

+    let listen = Command::new(&spt_bin)

+        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])

+    assert!(listen.status.success(), "api listen exit: {:?}", listen.status);

+    let stdout = String::from_utf8_lossy(&listen.stdout);

+        stdout.contains("delivered via listen"),

+        "listen must relay the spooled message, got: {stdout:?}"

+    // The perch was bound live with the seed's session id.

+    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after listen");

+    assert_eq!(rec.session_id, session_id);

+    assert_eq!(rec.state, "live_agent");


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && cargo test -p spt --test contract_e2e 2>&1 | tail -12

Run both E2E tests


thread 'mock_adapter_drives_the_full_contract' (56832) panicked at crates\spt\tests\contract_e2e.rs:92:5:
api poll exit: ExitStatus(ExitStatus(1))
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    mock_adapter_drives_the_full_contract

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s

error: test failed, to rerun pass `-p spt --test contract_e2e`


Both E2E tests race on the global `SPT_HOME`. Serialize them with a shared lock.


Edit

use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;

use std::path::PathBuf;
use std::process::Command;
use std::sync::Mutex;
use std::time::Duration;

/// Both tests mutate the process-global `SPT_HOME` (the in-test spt-store calls
/// and the spawned children read it). Serialize them so their homes don't race.
static E2E_LOCK: Mutex<()> = Mutex::new(());


use std::path::PathBuf;
use std::process::Command;
use std::sync::Mutex;
use std::time::Duration;

/// Both tests mutate the process-global `SPT_HOME` (the in-test spt-store calls
/// and the spawned children read it). Serialize them so their homes don't race.
static E2E_LOCK: Mutex<()> = Mutex::new(());

//! M2a contract E2E (T9): the inbound + outbound halves of the harness contract
//! proven end-to-end through the **generic mock adapter** — no vendor harness.
//!
//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`
//! helper → the helper fires `spt api bind` (skeleton→live) then
//! `spt api state idle` → a message is spooled to the perch → `spt api poll`
//! drains it. Asserts the round-trip.
//!
//! A second test exercises the **harness-hosted startup** topology proper —
//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered
//! message — the REQ-START-2 path.
//
// [int->REQ-API-1]
// [int->REQ-API-2]

use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;

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

/// Absolute path to a sibling binary in the same target dir as the `spt` test bin.
fn sibling_bin(name: &str) -> PathBuf {
    let spt = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    spt.with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

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

#[test]
fn mock_adapter_drives_the_full_contract() {
    // Isolated home shared by this process *and* the child `spt` invocations
    // (inherited via env). This test owns the process, so a plain set_var is safe.
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at
    // the freshly-built helper + spt binary (PATH isn't set up in the test rig).
    let manifest_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../adapters/mock/manifest.toml");
    let mut manifest = Manifest::from_toml_str(
        &std::fs::read_to_string(manifest_path).expect("read mock manifest"),
    )
    .expect("mock manifest valid");
    manifest.session.self_.as_mut().unwrap().command = format!(
        "\"{}\" --id {{id}} --session-id {{session_id}} --spt \"{}\"",
        mock_bin.display(),
        spt_bin.display(),
    );

    let runtime = ManifestRuntime::new(manifest);
    let id = "mock-e2e";
    let session_id = "sid-e2e-1";

    // Outbound: spawn the session. Bounded (not detached) so the test waits for
    // the helper's bind + state-idle to complete before asserting.
    let out = runtime
        .run_bounded("self", &keys(&[("id", id), ("session_id", session_id)]), Duration::from_secs(30))
        .expect("spawn mock session");
    assert!(
        out.success(),
        "mock-session failed: code={:?}\nstdout={}\nstderr={}",
        out.status_code, out.stdout, out.stderr
    );

    // Inbound assertion 1 — `api bind` flipped the perch live (REQ-START-2 /
    // REQ-SEAM-POSTSPAWN): info.json exists with the reported session id.
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after bind");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");

    // Inbound assertion 2 — `api state idle` wrote the activity sentinel.
    assert!(perch_path.join(".idle").exists(), "state idle must write the .idle sentinel");

    // A peer spools a message to the now-live perch.
    spool::spool_message_at(&perch_path, "tester", "hello over the contract").unwrap();

    // Inbound assertion 3 — `api poll` drains it (the full delivery round-trip).
    let poll = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api poll");
    assert!(poll.status.success(), "api poll exit: {:?}", poll.status);
    let stdout = String::from_utf8_lossy(&poll.stdout);
    assert!(
        stdout.contains("hello over the contract"),
        "api poll must deliver the spooled message, got: {stdout:?}"
    );

    // Auth negative — a poll with no proof is refused (REQ-HAZARD-LOCAL-API-AUTH
    // exercised through the real binary).
    let unauth = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id])
        .env("SPT_HOME", home.path())
        .env_remove("OWL_SESSION_ID")
        .output()
        .expect("run unauth poll");
    assert!(!unauth.status.success(), "poll without proof must be refused");
}

/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed
/// (matched by parent pid, validated against pid-recycle), binds the perch
/// skeleton→live, and relays a delivered message.
#[test]
fn seed_then_listen_binds_and_relays() {
    // [int->REQ-START-2]
    // [int->REQ-API-2]
    let home = tempfile::tempdir().unwrap();
    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let id = "listen-e2e";
    let session_id = "sid-listen-1";
    // The anchor must be a live process; the test runner itself qualifies.
    let anchor = std::process::id().to_string();

    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();

    // Seed the startup record keyed by the anchor pid.
    let seed = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .status()
        .expect("run api seed");
    assert!(seed.success(), "api seed exit: {seed:?}");

    // Listen consumes the seed (explicit --parent-pid for a deterministic match),
    // binds the perch, drains the backlog, and exits (--once).
    let listen = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api listen");
    assert!(listen.status.success(), "api listen exit: {:?}", listen.status);
    let stdout = String::from_utf8_lossy(&listen.stdout);
    assert!(
        stdout.contains("delivered via listen"),
        "listen must relay the spooled message, got: {stdout:?}"
    );

    // The perch was bound live with the seed's session id.
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after listen");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");
}

+/// Both tests mutate the process-global `SPT_HOME` (the in-test spt-store calls

+/// and the spawned children read it). Serialize them so their homes don't race.

+static E2E_LOCK: Mutex<()> = Mutex::new(());


fn mock_adapter_drives_the_full_contract() {
    // Isolated home shared by this process *and* the child `spt` invocations

fn mock_adapter_drives_the_full_contract() {
    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    // Isolated home shared by this process *and* the child `spt` invocations


fn mock_adapter_drives_the_full_contract() {
    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    // Isolated home shared by this process *and* the child `spt` invocations

//! M2a contract E2E (T9): the inbound + outbound halves of the harness contract
//! proven end-to-end through the **generic mock adapter** — no vendor harness.
//!
//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`
//! helper → the helper fires `spt api bind` (skeleton→live) then
//! `spt api state idle` → a message is spooled to the perch → `spt api poll`
//! drains it. Asserts the round-trip.
//!
//! A second test exercises the **harness-hosted startup** topology proper —
//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered
//! message — the REQ-START-2 path.
//
// [int->REQ-API-1]
// [int->REQ-API-2]

use std::path::PathBuf;
use std::process::Command;
use std::sync::Mutex;
use std::time::Duration;

/// Both tests mutate the process-global `SPT_HOME` (the in-test spt-store calls
/// and the spawned children read it). Serialize them so their homes don't race.
static E2E_LOCK: Mutex<()> = Mutex::new(());

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

/// Absolute path to a sibling binary in the same target dir as the `spt` test bin.
fn sibling_bin(name: &str) -> PathBuf {
    let spt = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    spt.with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

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

#[test]
fn mock_adapter_drives_the_full_contract() {
    // Isolated home shared by this process *and* the child `spt` invocations
    // (inherited via env). This test owns the process, so a plain set_var is safe.
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at
    // the freshly-built helper + spt binary (PATH isn't set up in the test rig).
    let manifest_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../adapters/mock/manifest.toml");
    let mut manifest = Manifest::from_toml_str(
        &std::fs::read_to_string(manifest_path).expect("read mock manifest"),
    )
    .expect("mock manifest valid");
    manifest.session.self_.as_mut().unwrap().command = format!(
        "\"{}\" --id {{id}} --session-id {{session_id}} --spt \"{}\"",
        mock_bin.display(),
        spt_bin.display(),
    );

    let runtime = ManifestRuntime::new(manifest);
    let id = "mock-e2e";
    let session_id = "sid-e2e-1";

    // Outbound: spawn the session. Bounded (not detached) so the test waits for
    // the helper's bind + state-idle to complete before asserting.
    let out = runtime
        .run_bounded("self", &keys(&[("id", id), ("session_id", session_id)]), Duration::from_secs(30))
        .expect("spawn mock session");
    assert!(
        out.success(),
        "mock-session failed: code={:?}\nstdout={}\nstderr={}",
        out.status_code, out.stdout, out.stderr
    );

    // Inbound assertion 1 — `api bind` flipped the perch live (REQ-START-2 /
    // REQ-SEAM-POSTSPAWN): info.json exists with the reported session id.
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after bind");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");

    // Inbound assertion 2 — `api state idle` wrote the activity sentinel.
    assert!(perch_path.join(".idle").exists(), "state idle must write the .idle sentinel");

    // A peer spools a message to the now-live perch.
    spool::spool_message_at(&perch_path, "tester", "hello over the contract").unwrap();

    // Inbound assertion 3 — `api poll` drains it (the full delivery round-trip).
    let poll = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api poll");
    assert!(poll.status.success(), "api poll exit: {:?}", poll.status);
    let stdout = String::from_utf8_lossy(&poll.stdout);
    assert!(
        stdout.contains("hello over the contract"),
        "api poll must deliver the spooled message, got: {stdout:?}"
    );

    // Auth negative — a poll with no proof is refused (REQ-HAZARD-LOCAL-API-AUTH
    // exercised through the real binary).
    let unauth = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id])
        .env("SPT_HOME", home.path())
        .env_remove("OWL_SESSION_ID")
        .output()
        .expect("run unauth poll");
    assert!(!unauth.status.success(), "poll without proof must be refused");
}

/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed
/// (matched by parent pid, validated against pid-recycle), binds the perch
/// skeleton→live, and relays a delivered message.
#[test]
fn seed_then_listen_binds_and_relays() {
    // [int->REQ-START-2]
    // [int->REQ-API-2]
    let home = tempfile::tempdir().unwrap();
    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let id = "listen-e2e";
    let session_id = "sid-listen-1";
    // The anchor must be a live process; the test runner itself qualifies.
    let anchor = std::process::id().to_string();

    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();

    // Seed the startup record keyed by the anchor pid.
    let seed = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .status()
        .expect("run api seed");
    assert!(seed.success(), "api seed exit: {seed:?}");

    // Listen consumes the seed (explicit --parent-pid for a deterministic match),
    // binds the perch, drains the backlog, and exits (--once).
    let listen = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api listen");
    assert!(listen.status.success(), "api listen exit: {:?}", listen.status);
    let stdout = String::from_utf8_lossy(&listen.stdout);
    assert!(
        stdout.contains("delivered via listen"),
        "listen must relay the spooled message, got: {stdout:?}"
    );

    // The perch was bound live with the seed's session id.
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after listen");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");
}

+    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());


fn seed_then_listen_binds_and_relays() {
    // [int->REQ-START-2]
    // [int->REQ-API-2]
    let home = tempfile::tempdir().unwrap();

fn seed_then_listen_binds_and_relays() {
    // [int->REQ-START-2]
    // [int->REQ-API-2]
    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let home = tempfile::tempdir().unwrap();


fn seed_then_listen_binds_and_relays() {
    // [int->REQ-START-2]
    // [int->REQ-API-2]
    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let home = tempfile::tempdir().unwrap();

//! M2a contract E2E (T9): the inbound + outbound halves of the harness contract
//! proven end-to-end through the **generic mock adapter** — no vendor harness.
//!
//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`
//! helper → the helper fires `spt api bind` (skeleton→live) then
//! `spt api state idle` → a message is spooled to the perch → `spt api poll`
//! drains it. Asserts the round-trip.
//!
//! A second test exercises the **harness-hosted startup** topology proper —
//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered
//! message — the REQ-START-2 path.
//
// [int->REQ-API-1]
// [int->REQ-API-2]

use std::path::PathBuf;
use std::process::Command;
use std::sync::Mutex;
use std::time::Duration;

/// Both tests mutate the process-global `SPT_HOME` (the in-test spt-store calls
/// and the spawned children read it). Serialize them so their homes don't race.
static E2E_LOCK: Mutex<()> = Mutex::new(());

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

/// Absolute path to a sibling binary in the same target dir as the `spt` test bin.
fn sibling_bin(name: &str) -> PathBuf {
    let spt = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    spt.with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

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

#[test]
fn mock_adapter_drives_the_full_contract() {
    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    // Isolated home shared by this process *and* the child `spt` invocations
    // (inherited via env). This test owns the process, so a plain set_var is safe.
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let mock_bin = sibling_bin("mock-session");
    assert!(mock_bin.exists(), "mock-session must be built: {}", mock_bin.display());

    // Load the canonical mock manifest, then point its [session.self] template at
    // the freshly-built helper + spt binary (PATH isn't set up in the test rig).
    let manifest_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../adapters/mock/manifest.toml");
    let mut manifest = Manifest::from_toml_str(
        &std::fs::read_to_string(manifest_path).expect("read mock manifest"),
    )
    .expect("mock manifest valid");
    manifest.session.self_.as_mut().unwrap().command = format!(
        "\"{}\" --id {{id}} --session-id {{session_id}} --spt \"{}\"",
        mock_bin.display(),
        spt_bin.display(),
    );

    let runtime = ManifestRuntime::new(manifest);
    let id = "mock-e2e";
    let session_id = "sid-e2e-1";

    // Outbound: spawn the session. Bounded (not detached) so the test waits for
    // the helper's bind + state-idle to complete before asserting.
    let out = runtime
        .run_bounded("self", &keys(&[("id", id), ("session_id", session_id)]), Duration::from_secs(30))
        .expect("spawn mock session");
    assert!(
        out.success(),
        "mock-session failed: code={:?}\nstdout={}\nstderr={}",
        out.status_code, out.stdout, out.stderr
    );

    // Inbound assertion 1 — `api bind` flipped the perch live (REQ-START-2 /
    // REQ-SEAM-POSTSPAWN): info.json exists with the reported session id.
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after bind");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");

    // Inbound assertion 2 — `api state idle` wrote the activity sentinel.
    assert!(perch_path.join(".idle").exists(), "state idle must write the .idle sentinel");

    // A peer spools a message to the now-live perch.
    spool::spool_message_at(&perch_path, "tester", "hello over the contract").unwrap();

    // Inbound assertion 3 — `api poll` drains it (the full delivery round-trip).
    let poll = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api poll");
    assert!(poll.status.success(), "api poll exit: {:?}", poll.status);
    let stdout = String::from_utf8_lossy(&poll.stdout);
    assert!(
        stdout.contains("hello over the contract"),
        "api poll must deliver the spooled message, got: {stdout:?}"
    );

    // Auth negative — a poll with no proof is refused (REQ-HAZARD-LOCAL-API-AUTH
    // exercised through the real binary).
    let unauth = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "poll", id])
        .env("SPT_HOME", home.path())
        .env_remove("OWL_SESSION_ID")
        .output()
        .expect("run unauth poll");
    assert!(!unauth.status.success(), "poll without proof must be refused");
}

/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed
/// (matched by parent pid, validated against pid-recycle), binds the perch
/// skeleton→live, and relays a delivered message.
#[test]
fn seed_then_listen_binds_and_relays() {
    // [int->REQ-START-2]
    // [int->REQ-API-2]
    let home = tempfile::tempdir().unwrap();
    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let id = "listen-e2e";
    let session_id = "sid-listen-1";
    // The anchor must be a live process; the test runner itself qualifies.
    let anchor = std::process::id().to_string();

    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.
    std::env::set_var("SPT_HOME", home.path());
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    spool::spool_message_at(&perch_path, "tester", "delivered via listen").unwrap();

    // Seed the startup record keyed by the anchor pid.
    let seed = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "seed", "--pid", &anchor, "--session-id", session_id])
        .env("SPT_HOME", home.path())
        .status()
        .expect("run api seed");
    assert!(seed.success(), "api seed exit: {seed:?}");

    // Listen consumes the seed (explicit --parent-pid for a deterministic match),
    // binds the perch, drains the backlog, and exits (--once).
    let listen = Command::new(&spt_bin)
        .args(["api", "--adapter", "mock", "listen", id, "--parent-pid", &anchor, "--once"])
        .env("SPT_HOME", home.path())
        .output()
        .expect("run api listen");
    assert!(listen.status.success(), "api listen exit: {:?}", listen.status);
    let stdout = String::from_utf8_lossy(&listen.stdout);
    assert!(
        stdout.contains("delivered via listen"),
        "listen must relay the spooled message, got: {stdout:?}"
    );

    // The perch was bound live with the seed's session id.
    let rec = spt_store::info::read_info(&perch_path).expect("perch info.json after listen");
    assert_eq!(rec.session_id, session_id);
    assert_eq!(rec.state, "live_agent");
}


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && cargo test -p spt --test contract_e2e 2>&1 | tail -10

Re-run E2E tests serialized


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.88s
     Running tests\contract_e2e.rs (target\debug\deps\contract_e2e-0e9cfdca991b371f.exe)

running 2 tests
test mock_adapter_drives_the_full_contract ... ok
SEEDED:54740
test seed_then_listen_binds_and_relays ... ok

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


Both E2E pass. Verify check + clippy, then commit T9.


cd "C:\Users\decid\Documents\projects\spt-core" && traceable-reqs check 2>&1 | grep -E "REQ-API-1 |REQ-API-2 |REQ-START-2 "; echo "exit:"; traceable-reqs check >/dev/null 2>&1; echo $?; echo "---clippy---"; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -3

Check int coverage + clippy


  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-API-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-START-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
exit:
0
---clippy---
    Checking spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s


Updated task #9 status


Updated task #10 status


Updated task #11 status


cd "C:\Users\decid\Documents\projects\spt-core" && git add -A && git commit -q -F - <<'EOF'
test(spt): M2a T9 — contract E2E through the mock adapter

Two integration tests prove both halves of the harness contract end-to-end,
harness-agnostic (the generic mock adapter, no vendor harness):

1. mock_adapter_drives_the_full_contract — ManifestRuntime spawns mock-session
   (outbound) → the helper fires `api bind` (skeleton→live) + `api state idle`
   → a peer spools a message → `api poll` drains it. Asserts info.json (live +
   session id), the .idle sentinel, the delivered message, and that an
   unauthenticated poll is refused (local-api-auth through the real binary).
2. seed_then_listen_binds_and_relays — the harness-hosted startup proper:
   `api seed` → `api listen --once` consumes the seed (parent-pid match,
   pid-recycle validated), binds, and relays the spooled message.

A shared lock serializes the two (both mutate process-global SPT_HOME).
Cleaned dead code (BindError::NoParentPid, has_live_children) and redundant
closures; allow(dead_code) on the M2b-consumed forward APIs (resolve_filedrops,
env_injections, echo_gate_armed).

REQ-API-1/API-2/START-2 now carry `int`; all activated [impl, unit, int].
Full workspace green; clippy -D warnings green; traceable-reqs check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log --oneline -1

Commit T9


warning: in the working copy of 'crates/spt/src/api/delivery.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/api/mod.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/api/reporting.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/api/startup.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/api/worker.rs', 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
warning: in the working copy of 'crates/spt/tests/contract_e2e.rs', LF will be replaced by CRLF the next time Git touches it
504b346 test(spt): M2a T9 — contract E2E through the mock adapter


T9 done. Now T10 — activation sweep. Remaining: REQ-ARCH-2 (SDK surface gains spt-runtime). Check its state + the SDK-surface evidence.


cd "C:\Users\decid\Documents\projects\spt-core" && grep -n "REQ-ARCH-2" -A2 traceable-reqs.toml; echo "===existing ARCH-2 tags==="; grep -rn "REQ-ARCH-2\|ARCH-1" crates/ docs/ | head; echo "===check status==="; traceable-reqs check 2>&1 | grep -iE "ARCH-2|FAIL|MISSING|partial" | head

Inspect REQ-ARCH-2 + SDK evidence


27:id = "REQ-ARCH-2"
28-title = "Public SDK surface is spt-proto, spt-runtime, spt-msg"
29-required_stages = []
===existing ARCH-2 tags===
crates/spt-msg/src/lib.rs:3://! Third layer of the acyclically-layered crate stack (R-ARCH-1): depends only
crates/spt-msg/src/lib.rs:16:// [impl->REQ-ARCH-1]
crates/spt-proto/src/lib.rs:10:// (R-ARCH-1): it depends on no other workspace crate, so nothing it exposes can
crates/spt-proto/src/lib.rs:12:// [impl->REQ-ARCH-1]
crates/spt-store/src/lib.rs:3://! Sits directly above `spt-proto` (R-ARCH-1 layering): it serializes proto
crates/spt-store/src/lib.rs:9:// Second layer of the acyclically-layered crate stack (R-ARCH-1): spt-store
crates/spt-store/src/lib.rs:12:// [impl->REQ-ARCH-1]
===check status===
  [OK] REQ-ARCH-2  required: []  stages: -doc -impl -unit -int
       Stale registry entries degrade to fallback, never hard-fail (4.3)


REQ-ARCH-2 needs activation + an SDK-surface impl tag. Add tag to spt-runtime, then check the CI gate composition.


1	name: ci
2
3	on:
4	  push:
5	    branches: [main, dev-freeform]
6	  pull_request:
7
8	# Cancel superseded runs on the same ref.
9	concurrency:
10	  group: ci-${{ github.ref }}
11	  cancel-in-progress: true
12
13	jobs:
14	  # ---- Rust build + test + lint (cross-platform: exercises both the
15	  # cfg(windows) OpenProcess path and the cfg(unix) libc path in proc.rs) ----
16	  test:
17	    strategy:
18	      fail-fast: false
19	      matrix:
20	        os: [ubuntu-latest, windows-latest]
21	    runs-on: ${{ matrix.os }}
22	    steps:
23	      - uses: actions/checkout@v4
24	      - name: Install Rust (stable + clippy)
25	        uses: dtolnay/rust-toolchain@stable
26	        with:
27	          components: clippy
28	      - name: Cache cargo
29	        uses: Swatinem/rust-cache@v2
30	      - name: Build
31	        run: cargo build --workspace --all-targets
32	      - name: Test
33	        run: cargo test --workspace
34	      - name: Clippy (deny warnings)
35	        run: cargo clippy --workspace --all-targets -- -D warnings
36
37	  # ---- Requirement-traceability hard gate (TRACEABILITY.md layer 1) ----
38	  traceability:
39	    runs-on: ubuntu-latest
40	    steps:
41	      - uses: actions/checkout@v4
42	      - name: Install traceable-reqs
43	        run: |
44	          gh release download --repo BigscreenVR/traceable-reqs \
45	            --pattern '*linux-x86_64' --output traceable-reqs
46	          chmod +x traceable-reqs
47	        env:
48	          GH_TOKEN: ${{ github.token }}
49	      - name: Coverage gate (hard — fails PR on uncovered activated req)
50	        run: ./traceable-reqs check --json
51	      - name: Quality audit (advisory)
52	        run: ./traceable-reqs lint || true
53

name: ci

on:
  push:
    branches: [main, dev-freeform]
  pull_request:

# Cancel superseded runs on the same ref.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # ---- Rust build + test + lint (cross-platform: exercises both the
  # cfg(windows) OpenProcess path and the cfg(unix) libc path in proc.rs) ----
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - name: Install Rust (stable + clippy)
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - name: Cache cargo
        uses: Swatinem/rust-cache@v2
      - name: Build
        run: cargo build --workspace --all-targets
      - name: Test
        run: cargo test --workspace
      - name: Clippy (deny warnings)
        run: cargo clippy --workspace --all-targets -- -D warnings

  # ---- Requirement-traceability hard gate (TRACEABILITY.md layer 1) ----
  traceability:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install traceable-reqs
        run: |
          gh release download --repo BigscreenVR/traceable-reqs \
            --pattern '*linux-x86_64' --output traceable-reqs
          chmod +x traceable-reqs
        env:
          GH_TOKEN: ${{ github.token }}
      - name: Coverage gate (hard — fails PR on uncovered activated req)
        run: ./traceable-reqs check --json
      - name: Quality audit (advisory)
        run: ./traceable-reqs lint || true


CI gates = build/test/clippy + traceability. Add the REQ-ARCH-2 SDK tag and activate.


1	//! spt-core harness contract — outbound half (M2a).
2	//!
3	//! `spt-runtime` makes spt-core **drivable by a manifest-described harness** with
4	//! zero Claude Code (or any harness) conventions in-repo. It is the fourth layer
5	//! in the acyclic stack (`spt-proto -> spt-store -> spt-msg -> spt-runtime`).
6	//!
7	//! Two surfaces land here across M2a:
8	//! - the **runtime-manifest schema** + parse/validate ([`manifest`], T1), and
9	//! - the [`AgentRuntime`] trait + default `ManifestRuntime` (opaque-template
10	//!   substitution + spawn-session seam, T2).
11	//!
12	//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
13	//! over this crate. See `M2a-PLAN.md`.
14
15	pub mod manifest;
16	pub mod runtime;
17
18	pub use manifest::{Manifest, ManifestError};
19	pub use runtime::{AgentRuntime, ManifestRuntime, RuntimeError, Spawned};
20

//! spt-core harness contract — outbound half (M2a).
//!
//! `spt-runtime` makes spt-core **drivable by a manifest-described harness** with
//! zero Claude Code (or any harness) conventions in-repo. It is the fourth layer
//! in the acyclic stack (`spt-proto -> spt-store -> spt-msg -> spt-runtime`).
//!
//! Two surfaces land here across M2a:
//! - the **runtime-manifest schema** + parse/validate ([`manifest`], T1), and
//! - the [`AgentRuntime`] trait + default `ManifestRuntime` (opaque-template
//!   substitution + spawn-session seam, T2).
//!
//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
//! over this crate. See `M2a-PLAN.md`.

pub mod manifest;
pub mod runtime;

pub use manifest::{Manifest, ManifestError};
pub use runtime::{AgentRuntime, ManifestRuntime, RuntimeError, Spawned};