170	                shell_id,
171	                adapter_name,
172	                shell,
173	            )
174	        }
175	        _ => {
176	            let tokens = fill_spawn_command(owlery, owner, shell_id, adapter_name, shell)?;
177	            let (program, args) = tokens.split_first().expect("non-empty checked");
178	            // The shell binary is a LONG-LIVED detached child (KNOWN-HAZARDS
179	            // 5.6): on Windows it must inherit NO handles, or a caller
180	            // capturing `spt shell spawn`'s output — at any depth of the
181	            // process chain — hangs forever on a pipe write-end the immortal
182	            // child holds. Same no-inherit spawn as the daemon's.
183	            // [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]
184	            #[cfg(windows)]
185	            let pid = crate::daemon::detached_no_inherit(program, args)
186	                .map_err(|e| format!("spawn {program}: {e}"))?;
187	            #[cfg(unix)]
188	            let pid = Command::new(program)
189	                .args(args)
190	                .stdin(Stdio::null())
191	                .stdout(Stdio::null())
192	                .stderr(Stdio::null())
193	                .spawn()
194	                .map_err(|e| format!("spawn {program}: {e}"))?
195	                .id();
196	            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
197	            std::fs::write(perch.join(SHELL_PID_FILE), pid.to_string())
198	                .map_err(|e| format!("record pid: {e}"))?;
199	            Ok(pid)
200	        }
201	    }
202	}
203
204	/// The stdin-receipt launch: spawn the filled template as a **broker-hosted**
205	/// session labeled [`shell_session_label`], so the daemon owns the child's
206	/// stdin and `spt shell cmd` can drive it through the `send_effect` input
207	/// seam (exactly-once per spool row). The broker outlives this connection —
208	/// the session keeps running after the spawning call returns.
209	// [impl->REQ-SHELL-1]
210	pub fn launch_shell_brokered_in(
211	    broker_name: &str,
212	    owlery: &Path,
213	    owner: &str,
214	    shell_id: &str,
215	    adapter_name: &str,
216	    shell: &Shell,
217	) -> Result<u32, String> {
218	    let tokens = fill_spawn_command(owlery, owner, shell_id, adapter_name, shell)?;
219	    let (program, args) = tokens.split_first().expect("non-empty checked");
220	    let mut brain =
221	        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
222	    let (_sid, pid) = brain
223	        .spawn_session_pid(SpawnReq {
224	            program: program.clone(),
225	            args: args.to_vec(),
226	            // A shell binary is not a terminal: the PTY is only the stdin
227	            // conduit. Keep it wide so renderer-side wrapping (ConPTY) can
228	            // never split a >80-col stamped frame mid-line.
229	            rows: 24,
230	            cols: 512,
231	            endpoint: shell_session_label(owner, shell_id),
232	        })
233	        .map_err(|e| format!("broker spawn: {e}"))?;
234	    let pid = pid.unwrap_or(0);
235	    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
236	    std::fs::write(perch.join(SHELL_PID_FILE), pid.to_string())
237	        .map_err(|e| format!("record pid: {e}"))?;
238	    Ok(pid)
239	}
240
241	/// Resolve a shell instance by its link token alone — the "owner from the
242	/// link" lookup every shell-flavored machinery call shares. Scans every
243	/// owner's `shells/` namespace for the perch parking exactly this token; an
244	/// unknown/empty token resolves nothing. Pure read — onlining is the bind's
245	/// job ([`bind_shell_by_token`]), not the lookup's.
246	// [impl->REQ-SHELL-1]
247	pub fn find_shell_by_token(owlery: &Path, token: &str) -> Option<(String, String)> {
248	    if token.is_empty() {
249	        return None;
250	    }
251	    for owner in spt_store::perch::list_self_perch_ids(owlery) {
252	        for (shell_id, _info) in shellinfo::list_shells(owlery, &owner) {
253	            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);
254	            if read_link_token(&perch).as_deref() == Some(token) {
255	                return Some((owner, shell_id));
256	            }
257	        }
258	    }
259	    None
260	}
261
262	/// Resolve a bind by token alone and flip the instance online — the
263	/// handshake's receiving end. The spawn template carries only
264	/// `{link_token}`, so the binary never has to know its owner ("owner from
265	/// the link"). An unknown/empty token binds nothing. Returns
266	/// `(owner, shell_id)`.
267	// [impl->REQ-SHELL-1]
268	pub fn bind_shell_by_token(owlery: &Path, token: &str) -> Option<(String, String)> {
269	    let (owner, shell_id) = find_shell_by_token(owlery, token)?;
270	    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);
271	    let mut info = shellinfo::read_shell_info(&perch)?;
272	    info.status = SHELL_STATUS_ONLINE.to_string();
273	    shellinfo::write_shell_info(&perch, &info).ok()?;
274	    Some((owner, shell_id))
275	}
276
277	/// Verify a machinery call's link credential against one instance — the
278	/// per-message auth gate the D3c channel surfaces (`api emit`, `api poll`)
279	/// sit behind.
280	// [impl->REQ-SHELL-1]
281	pub fn verify_link(owlery: &Path, owner: &str, shell_id: &str, token: &str) -> bool {
282	    if token.is_empty() {
283	        return false;
284	    }
285	    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
286	    read_link_token(&perch).as_deref() == Some(token)
287	}
288
289	/// Best-effort kill of a launched shell binary (teardown's interim close —
290	/// the D4 lifecycle adds the manifest pre-close instruction + termination
291	/// timeout in front of this). Bounded; never blocks on a wedged child.
292	pub fn kill_shell_pid(pid: u32) {
293	    if pid == 0 {
294	        // A broker-hosted spawn whose backend exposed no pid records 0 —
295	        // never kill that (unix `kill 0` would signal the whole group).
296	        return;
297	    }
298	    #[cfg(windows)]
299	    let _ = Command::new("taskkill")
300	        .args(["/PID", &pid.to_string(), "/T", "/F"])
301	        .stdout(Stdio::null())
302	        .stderr(Stdio::null())
303	        .status();
304	    #[cfg(unix)]
305	    let _ = Command::new("kill")
306	        .args(["-9", &pid.to_string()])
307	        .stdout(Stdio::null())
308	        .stderr(Stdio::null())
309	        .status();
310	}
311
312	/// Teardown's process half: read the recorded pid (if any) and kill it. The
313	/// caller removes the perch dir after.
314	pub fn kill_shell_at(shell_perch: &Path) {

                shell_id,
                adapter_name,
                shell,
            )
        }
        _ => {
            let tokens = fill_spawn_command(owlery, owner, shell_id, adapter_name, shell)?;
            let (program, args) = tokens.split_first().expect("non-empty checked");
            // The shell binary is a LONG-LIVED detached child (KNOWN-HAZARDS
            // 5.6): on Windows it must inherit NO handles, or a caller
            // capturing `spt shell spawn`'s output — at any depth of the
            // process chain — hangs forever on a pipe write-end the immortal
            // child holds. Same no-inherit spawn as the daemon's.
            // [impl->REQ-HAZARD-DETACHED-PIPE-INHERIT]
            #[cfg(windows)]
            let pid = crate::daemon::detached_no_inherit(program, args)
                .map_err(|e| format!("spawn {program}: {e}"))?;
            #[cfg(unix)]
            let pid = Command::new(program)
                .args(args)
                .stdin(Stdio::null())
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .spawn()
                .map_err(|e| format!("spawn {program}: {e}"))?
                .id();
            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
            std::fs::write(perch.join(SHELL_PID_FILE), pid.to_string())
                .map_err(|e| format!("record pid: {e}"))?;
            Ok(pid)
        }
    }
}

/// The stdin-receipt launch: spawn the filled template as a **broker-hosted**
/// session labeled [`shell_session_label`], so the daemon owns the child's
/// stdin and `spt shell cmd` can drive it through the `send_effect` input
/// seam (exactly-once per spool row). The broker outlives this connection —
/// the session keeps running after the spawning call returns.
// [impl->REQ-SHELL-1]
pub fn launch_shell_brokered_in(
    broker_name: &str,
    owlery: &Path,
    owner: &str,
    shell_id: &str,
    adapter_name: &str,
    shell: &Shell,
) -> Result<u32, String> {
    let tokens = fill_spawn_command(owlery, owner, shell_id, adapter_name, shell)?;
    let (program, args) = tokens.split_first().expect("non-empty checked");
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let (_sid, pid) = brain
        .spawn_session_pid(SpawnReq {
            program: program.clone(),
            args: args.to_vec(),
            // A shell binary is not a terminal: the PTY is only the stdin
            // conduit. Keep it wide so renderer-side wrapping (ConPTY) can
            // never split a >80-col stamped frame mid-line.
            rows: 24,
            cols: 512,
            endpoint: shell_session_label(owner, shell_id),
        })
        .map_err(|e| format!("broker spawn: {e}"))?;
    let pid = pid.unwrap_or(0);
    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
    std::fs::write(perch.join(SHELL_PID_FILE), pid.to_string())
        .map_err(|e| format!("record pid: {e}"))?;
    Ok(pid)
}

/// Resolve a shell instance by its link token alone — the "owner from the
/// link" lookup every shell-flavored machinery call shares. Scans every
/// owner's `shells/` namespace for the perch parking exactly this token; an
/// unknown/empty token resolves nothing. Pure read — onlining is the bind's
/// job ([`bind_shell_by_token`]), not the lookup's.
// [impl->REQ-SHELL-1]
pub fn find_shell_by_token(owlery: &Path, token: &str) -> Option<(String, String)> {
    if token.is_empty() {
        return None;
    }
    for owner in spt_store::perch::list_self_perch_ids(owlery) {
        for (shell_id, _info) in shellinfo::list_shells(owlery, &owner) {
            let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);
            if read_link_token(&perch).as_deref() == Some(token) {
                return Some((owner, shell_id));
            }
        }
    }
    None
}

/// Resolve a bind by token alone and flip the instance online — the
/// handshake's receiving end. The spawn template carries only
/// `{link_token}`, so the binary never has to know its owner ("owner from
/// the link"). An unknown/empty token binds nothing. Returns
/// `(owner, shell_id)`.
// [impl->REQ-SHELL-1]
pub fn bind_shell_by_token(owlery: &Path, token: &str) -> Option<(String, String)> {
    let (owner, shell_id) = find_shell_by_token(owlery, token)?;
    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, &owner, &shell_id);
    let mut info = shellinfo::read_shell_info(&perch)?;
    info.status = SHELL_STATUS_ONLINE.to_string();
    shellinfo::write_shell_info(&perch, &info).ok()?;
    Some((owner, shell_id))
}

/// Verify a machinery call's link credential against one instance — the
/// per-message auth gate the D3c channel surfaces (`api emit`, `api poll`)
/// sit behind.
// [impl->REQ-SHELL-1]
pub fn verify_link(owlery: &Path, owner: &str, shell_id: &str, token: &str) -> bool {
    if token.is_empty() {
        return false;
    }
    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
    read_link_token(&perch).as_deref() == Some(token)
}

/// Best-effort kill of a launched shell binary (teardown's interim close —
/// the D4 lifecycle adds the manifest pre-close instruction + termination
/// timeout in front of this). Bounded; never blocks on a wedged child.
pub fn kill_shell_pid(pid: u32) {
    if pid == 0 {
        // A broker-hosted spawn whose backend exposed no pid records 0 —
        // never kill that (unix `kill 0` would signal the whole group).
        return;
    }
    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .args(["/PID", &pid.to_string(), "/T", "/F"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();
    #[cfg(unix)]
    let _ = Command::new("kill")
        .args(["-9", &pid.to_string()])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();
}

/// Teardown's process half: read the recorded pid (if any) and kill it. The
/// caller removes the perch dir after.
pub fn kill_shell_at(shell_perch: &Path) {


/diagnose every 60 seconds on this machine, 2 blank windows appear and disappear in quick succession. the SPT daemon is running, and i believe an endpoint is as well. need to suppress the blank window…


`taskkill` (shellhost.rs:299) lacks flag too — but only fires on teardown, not periodic. Check watcher interim.


<system-reminder>[Truncated: PARTIAL view — showing lines 1-10 of 34 total (31581 tokens, cap 25000). Call Read with offset=11 limit=10 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]</system-reminder>

1	20:09:01.809 NEW pid=40040 parent=23468(pwsh) name=ssh.exe cmd="C:\WINDOWS\system32\openssh\ssh.exe" david@gravity-linux "bash -lc 'pid=$(cat ~/.spt-core/daemon.pid 2>/dev/null || true); echo old_daemon=$pid; kill $pid 2>/dev/null || true; sleep 2; ps -p $pid -o pid,cmd 2>/dev/null || true; timeout 20s spt list 2>&1 || true; echo new_pid=$(cat ~/.spt-core/daemon.pid 2>/dev/null || true); ps -ef | grep spt | grep -v grep'"
2	20:09:03.645 NEW pid=69296 parent=26196(chrome) name=chrome.exe cmd="C:\Program Files\Google\Chrome\Application\chrome.exe" --type=renderer --extension-process --origin-trial-disabled-features=CanvasTextNg|WebAssemblyCustomDescriptors --no-pre-read-main-dll --video-capture-use-gpu-memory-buffer --lang=en-US --device-scale-factor=1 --num-raster-threads=4 --enable-main-frame-before-activation --renderer-client-id=361 --time-ticks-at-unix-epoch=-1779663903386072 --launch-time-ticks=1137840014008 --metrics-shmem-handle=15392,i,1953430668370687796,13115060377209732019,2097152 --field-trial-handle=1772,i,493029493025384843,6928064253496062946,262144 --variations-seed-version --pseudonymization-salt-handle=1764,i,4959870477303067751,10874210399729482291,4 --trace-process-track-uuid=3190709324583978983 --mojo-platform-channel-handle=2708 /prefetch:9
3	20:09:07.735 NEW pid=35932 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd=
4	20:09:07.742 NEW pid=70772 parent=35932(?) name=conhost.exe cmd=
5	20:09:07.742 NEW pid=14044 parent=35932(?) name=bash.exe cmd=
6	20:09:07.743 NEW pid=32940 parent=14044(?) name=node.exe cmd=
7	20:09:08.656 NEW pid=64928 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd="C:\Program Files\Git\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
8	20:09:08.656 NEW pid=33788 parent=64928(bash) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
9	20:09:08.657 NEW pid=14788 parent=64928(bash) name=bash.exe cmd="C:\Program Files\Git\bin\..\usr\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
10	20:09:08.657 NEW pid=75080 parent=14788(?) name=node.exe cmd=C:\nvm4w\nodejs\node.exe C:/Users/decid/.claude/hooks/gsd-statusline.js

20:09:01.809 NEW pid=40040 parent=23468(pwsh) name=ssh.exe cmd="C:\WINDOWS\system32\openssh\ssh.exe" david@gravity-linux "bash -lc 'pid=$(cat ~/.spt-core/daemon.pid 2>/dev/null || true); echo old_daemon=$pid; kill $pid 2>/dev/null || true; sleep 2; ps -p $pid -o pid,cmd 2>/dev/null || true; timeout 20s spt list 2>&1 || true; echo new_pid=$(cat ~/.spt-core/daemon.pid 2>/dev/null || true); ps -ef | grep spt | grep -v grep'"
20:09:03.645 NEW pid=69296 parent=26196(chrome) name=chrome.exe cmd="C:\Program Files\Google\Chrome\Application\chrome.exe" --type=renderer --extension-process --origin-trial-disabled-features=CanvasTextNg|WebAssemblyCustomDescriptors --no-pre-read-main-dll --video-capture-use-gpu-memory-buffer --lang=en-US --device-scale-factor=1 --num-raster-threads=4 --enable-main-frame-before-activation --renderer-client-id=361 --time-ticks-at-unix-epoch=-1779663903386072 --launch-time-ticks=1137840014008 --metrics-shmem-handle=15392,i,1953430668370687796,13115060377209732019,2097152 --field-trial-handle=1772,i,493029493025384843,6928064253496062946,262144 --variations-seed-version --pseudonymization-salt-handle=1764,i,4959870477303067751,10874210399729482291,4 --trace-process-track-uuid=3190709324583978983 --mojo-platform-channel-handle=2708 /prefetch:9
20:09:07.735 NEW pid=35932 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd=
20:09:07.742 NEW pid=70772 parent=35932(?) name=conhost.exe cmd=
20:09:07.742 NEW pid=14044 parent=35932(?) name=bash.exe cmd=
20:09:07.743 NEW pid=32940 parent=14044(?) name=node.exe cmd=
20:09:08.656 NEW pid=64928 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd="C:\Program Files\Git\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
20:09:08.656 NEW pid=33788 parent=64928(bash) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
20:09:08.657 NEW pid=14788 parent=64928(bash) name=bash.exe cmd="C:\Program Files\Git\bin\..\usr\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
20:09:08.657 NEW pid=75080 parent=14788(?) name=node.exe cmd=C:\nvm4w\nodejs\node.exe C:/Users/decid/.claude/hooks/gsd-statusline.js


No matches found


No git/spt children yet. Noise alert: claude statusline hook spawns bash+conhost — competing hypothesis. Wait full 75s capture.


ToolSearch

Question: select:Monitor


Question: select:Monitor


While waiting: count git calls per sync pull — peerloop + syncmerge.


crates\spt-store\src\branchstore.rs:28:use crate::gitrun::{run_git, run_git_ok, GIT_TIMEOUT};
crates\spt-store\src\branchstore.rs:51:            run_git_ok(&["init", "--bare", &git_dir.to_string_lossy()], None, None)?;
crates\spt-store\src\branchstore.rs:53:            run_git_ok(
crates\spt-store\src\branchstore.rs:59:            let _ = run_git(
crates\spt-store\src\branchstore.rs:77:        let out = run_git(
crates\spt-store\src\branchstore.rs:104:        let tree = run_git_ok(&["-C", &dir, "mktree"], None, Some(b""))?.stdout_trimmed();
crates\spt-store\src\branchstore.rs:105:        let seed = run_git_ok(
crates\spt-store\src\branchstore.rs:118:        run_git_ok(&["-C", &dir, "branch", branch, &seed], None, None)?;
crates\spt-store\src\branchstore.rs:134:        run_git_ok(
crates\spt-store\src\branchstore.rs:153:        run_git_ok(&["-C", &self.dir_arg(), "worktree", "repair"], None, None)?;
crates\spt-store\src\branchstore.rs:160:        let out = run_git_ok(
crates\spt-store\src\branchstore.rs:183:        let out = run_git(
crates\spt-store\src\branchstore.rs:214:        let out = run_git(
crates\spt-store\src\branchstore.rs:235:        let out = run_git_ok(
crates\spt-store\src\branchstore.rs:256:        let out = run_git(
crates\spt-store\src\branchstore.rs:273:        let out = run_git(
crates\spt-store\src\branchstore.rs:293:        run_git_ok(
crates\spt-store\src\branchstore.rs:340:        run_git_ok(&args, None, None)?;
crates\spt-store\src\branchstore.rs:352:        let heads = run_git_ok(
crates\spt-store\src\branchstore.rs:367:            run_git_ok(
crates\spt-store\src\branchstore.rs:379:            let tip = run_git_ok(
crates\spt-store\src\branchstore.rs:395:        run_git_ok(&["merge", "--ff-only", commit], Some(worktree), None)?;
crates\spt-store\src\branchstore.rs:413:        run_git_ok(&["add", "-A", "--", "."], Some(worktree), None)?;
crates\spt-store\src\branchstore.rs:414:        let tree = run_git_ok(&["write-tree"], Some(worktree), None)?.stdout_trimmed();
crates\spt-store\src\branchstore.rs:415:        let head = run_git_ok(&["rev-parse", "HEAD"], Some(worktree), None)?.stdout_trimmed();
crates\spt-store\src\branchstore.rs:416:        let merge = run_git_ok(
crates\spt-store\src\branchstore.rs:433:        run_git_ok(&["update-ref", "HEAD", &merge, &head], Some(worktree), None)?;
crates\spt-store\src\branchstore.rs:454:        run_git_ok(&add, Some(worktree), None)?;
crates\spt-store\src\branchstore.rs:455:        let staged = run_git(
crates\spt-store\src\branchstore.rs:464:        run_git_ok(&["commit", "-m", message], Some(worktree), None)?;
crates\spt-store\src\branchstore.rs:465:        let id = run_git_ok(&["rev-parse", "HEAD"], Some(worktree), None)?.stdout_trimmed();
crates\spt-store\src\branchstore.rs:619:        let count = run_git_ok(
crates\spt-store\src\contextstore.rs:282:                crate::gitrun::run_git_ok(
crates\spt-store\src\contextstore.rs:295:            crate::gitrun::run_git_ok(
crates\spt-store\src\contextstore.rs:317:                crate::gitrun::run_git_ok(&["mv", old_id, new_id], Some(&wt), None)?;
crates\spt-store\src\contextstore.rs:391:                crate::gitrun::run_git_ok(
crates\spt-store\src\contextstore.rs:404:            crate::gitrun::run_git_ok(
crates\spt-store\src\contextstore.rs:423:                crate::gitrun::run_git_ok(&["rm", "-r", id], Some(&wt), None)?;
crates\spt-store\src\gitrun.rs:49:pub fn run_git(
crates\spt-store\src\gitrun.rs:133:/// [`run_git`] that promotes a non-zero exit to an `io::Error` carrying git's
crates\spt-store\src\gitrun.rs:135:pub fn run_git_ok(
crates\spt-store\src\gitrun.rs:140:    let out = run_git(args, cwd, stdin, GIT_TIMEOUT)?;
crates\spt-store\src\gitrun.rs:158:        let out = run_git(&["--version"], None, None, GIT_TIMEOUT).expect("git on PATH");
crates\spt-store\src\gitrun.rs:169:        let res = run_git(&["--version"], None, None, Duration::from_millis(0));
crates\spt-store\src\project.rs:21:use crate::gitrun::run_git;
crates\spt-store\src\project.rs:68:    if let Ok(out) = run_git(
crates\spt-store\src\project.rs:83:    if let Ok(out) = run_git(
crates\spt-store\src\project.rs:117:    use crate::gitrun::run_git_ok;
crates\spt-store\src\project.rs:140:        run_git_ok(&["init", &root.to_string_lossy()], None, None).unwrap();
crates\spt-store\src\project.rs:144:        run_git_ok(


120
121	impl PumpPaths {
122	    /// The canonical SPT_HOME layout.
123	    pub fn production() -> PumpPaths {
124	        let home = perch::spt_home();
125	        let identity = perch::identity_dir();
126	        PumpPaths {
127	            owlery: perch::owlery_dir(),
128	            ctx_root: home.join("context"),
129	            notif_db: identity.join("notifs.db"),
130	            scratch: home.join("scratch").join("pump"),
131	            subnets: identity.join("subnet.json"),
132	            trust: identity.join("trust").join("peers.json"),
133	            vis: identity.join("visibility.json"),
134	            ops: identity.join("pump-ops.json"),
135	            releases: home.join("releases"),
136	            release_keys: identity.join("release-keys.json"),
137	            pull_marker: identity.join(crate::resting::PULL_MARKER_FILE),
138	        }
139	    }
140	}
141
142	/// Pump cadences (config-driven in production: [`DaemonConfig`]).
143	#[derive(Debug, Clone, Copy)]
144	pub struct PumpCadence {
145	    pub registry: Duration,
146	    pub notif: Duration,
147	    pub sync: Duration,
148	    pub update: Duration,
149	    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): swept each
150	    /// registry tick, before re-advertisement.
151	    pub registry_evict_after: Duration,
152	}
153
154	impl PumpCadence {
155	    /// From the daemon config knobs.
156	    pub fn from_config(cfg: &DaemonConfig) -> PumpCadence {
157	        PumpCadence {
158	            registry: cfg.registry_pump_period,
159	            notif: cfg.notif_pump_period,
160	            sync: cfg.sync_pull_period,
161	            update: cfg.update_check_period,
162	            registry_evict_after: cfg.registry_evict_after,
163	        }
164	    }
165	}
166
167	/// The pump's resolved knobs: cadences + the REQ-UPD-4 consent stance.
168	#[derive(Debug, Clone, Copy)]
169	pub struct PumpConfig {
170	    pub cadence: PumpCadence,
171	    /// `true` = the explicit opt-in to prompt-free updates; default `false`
172	    /// surfaces every staged release as a consent notif.
173	    pub full_auto_update: bool,
174	}
175
176	impl PumpConfig {
177	    /// From the daemon config knobs.
178	    pub fn from_config(cfg: &DaemonConfig) -> PumpConfig {
179	        PumpConfig {
180	            cadence: PumpCadence::from_config(cfg),
181	            full_auto_update: cfg.full_auto_update,
182	        }
183	    }
184	}
185
186	/// A pull observer: `(peer_hex, report)` after every completed sync pull.
187	pub type OnPull = Box<dyn FnMut(&str, &SyncPullReport) + Send>;
188
189	/// Observation hooks. `on_pull` fires after every completed sync pull —
190	/// the conflict surface (ADR-0013) and the E2E's probe.
191	pub struct PumpHooks {
192	    pub on_pull: OnPull,
193	}
194
195	impl Default for PumpHooks {
196	    /// The production hook: conflicts are loud, never silent — the artifacts
197	    /// already persist (hazard 6.6); the reconcile turn runs at activation.
198	    fn default() -> PumpHooks {
199	        PumpHooks {
200	            on_pull: Box::new(|peer, report| {
201	                for (branch, file) in report.conflicts() {
202	                    eprintln!("SYNC_CONFLICT:{peer}:{branch}:{file}");
203	                }
204	            }),
205	        }
206	    }
207	}
208
209	/// Run the pump until `stop` is set. Owns one brain IPC connection and a

impl PumpPaths {
    /// The canonical SPT_HOME layout.
    pub fn production() -> PumpPaths {
        let home = perch::spt_home();
        let identity = perch::identity_dir();
        PumpPaths {
            owlery: perch::owlery_dir(),
            ctx_root: home.join("context"),
            notif_db: identity.join("notifs.db"),
            scratch: home.join("scratch").join("pump"),
            subnets: identity.join("subnet.json"),
            trust: identity.join("trust").join("peers.json"),
            vis: identity.join("visibility.json"),
            ops: identity.join("pump-ops.json"),
            releases: home.join("releases"),
            release_keys: identity.join("release-keys.json"),
            pull_marker: identity.join(crate::resting::PULL_MARKER_FILE),
        }
    }
}

/// Pump cadences (config-driven in production: [`DaemonConfig`]).
#[derive(Debug, Clone, Copy)]
pub struct PumpCadence {
    pub registry: Duration,
    pub notif: Duration,
    pub sync: Duration,
    pub update: Duration,
    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): swept each
    /// registry tick, before re-advertisement.
    pub registry_evict_after: Duration,
}

impl PumpCadence {
    /// From the daemon config knobs.
    pub fn from_config(cfg: &DaemonConfig) -> PumpCadence {
        PumpCadence {
            registry: cfg.registry_pump_period,
            notif: cfg.notif_pump_period,
            sync: cfg.sync_pull_period,
            update: cfg.update_check_period,
            registry_evict_after: cfg.registry_evict_after,
        }
    }
}

/// The pump's resolved knobs: cadences + the REQ-UPD-4 consent stance.
#[derive(Debug, Clone, Copy)]
pub struct PumpConfig {
    pub cadence: PumpCadence,
    /// `true` = the explicit opt-in to prompt-free updates; default `false`
    /// surfaces every staged release as a consent notif.
    pub full_auto_update: bool,
}

impl PumpConfig {
    /// From the daemon config knobs.
    pub fn from_config(cfg: &DaemonConfig) -> PumpConfig {
        PumpConfig {
            cadence: PumpCadence::from_config(cfg),
            full_auto_update: cfg.full_auto_update,
        }
    }
}

/// A pull observer: `(peer_hex, report)` after every completed sync pull.
pub type OnPull = Box<dyn FnMut(&str, &SyncPullReport) + Send>;

/// Observation hooks. `on_pull` fires after every completed sync pull —
/// the conflict surface (ADR-0013) and the E2E's probe.
pub struct PumpHooks {
    pub on_pull: OnPull,
}

impl Default for PumpHooks {
    /// The production hook: conflicts are loud, never silent — the artifacts
    /// already persist (hazard 6.6); the reconcile turn runs at activation.
    fn default() -> PumpHooks {
        PumpHooks {
            on_pull: Box::new(|peer, report| {
                for (branch, file) in report.conflicts() {
                    eprintln!("SYNC_CONFLICT:{peer}:{branch}:{file}");
                }
            }),
        }
    }
}

/// Run the pump until `stop` is set. Owns one brain IPC connection and a


1:20:09:01.809 NEW pid=40040 parent=23468(pwsh) name=ssh.exe cmd="C:\WINDOWS\system32\openssh\ssh.exe" david@gravity-linux "bash -lc 'pid=$(cat ~/.spt-core/daemon.pid 2>/dev/null || true); echo old_daemon=$pid; kill $pid 2>/dev/null || true; sleep 2; ps -p $pid -o pid,cmd 2>/dev/null || true; timeout 20s spt list 2>&1 || true; echo new_pid=$(cat ~/.spt-core/daemon.pid 2>/dev/null || true); ps -ef | grep spt | grep -v grep'"
4:20:09:07.742 NEW pid=70772 parent=35932(?) name=conhost.exe cmd=
7:20:09:08.656 NEW pid=64928 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd="C:\Program Files\Git\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
8:20:09:08.656 NEW pid=33788 parent=64928(bash) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
9:20:09:08.657 NEW pid=14788 parent=64928(bash) name=bash.exe cmd="C:\Program Files\Git\bin\..\usr\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
12:20:09:09.562 NEW pid=39784 parent=75772(pwsh) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
14:20:09:09.800 NEW pid=17108 parent=41028(claude.exe.old.1780610438023) name=pwsh.exe cmd="C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-Process | Where-Object { $_.ProcessName -match 'spt|owl' } | Select-Object Id, ProcessName, Path, StartTime | Format-Table -AutoSize
18:20:09:10.536 NEW pid=48124 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd="C:\Program Files\Git\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
19:20:09:10.536 NEW pid=64200 parent=48124(bash) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
21:$ErrorActionPreference='Continue'; ssh david@gravity-linux 'bash -lc ''nohup spt daemon > /tmp/spt-daemon-rollout.log 2>&1 & sleep 3; echo daemon_log; cat /tmp/spt-daemon-rollout.log; echo pid_file=$(cat ~/.spt-core/daemon.pid 2>/dev/null || true); ps -ef | grep spt | grep -v grep; timeout 20s spt subnet status BIGNET --nodes 2>&1 || true'''"
22:20:09:13.969 NEW pid=69604 parent=18220(pwsh) name=ssh.exe cmd="C:\WINDOWS\system32\openssh\ssh.exe" david@gravity-linux "bash -lc 'nohup spt daemon > /tmp/spt-daemon-rollout.log 2>&1 & sleep 3; echo daemon_log; cat /tmp/spt-daemon-rollout.log; echo pid_file=$(cat ~/.spt-core/daemon.pid 2>/dev/null || true); ps -ef | grep spt | grep -v grep; timeout 20s spt subnet status BIGNET --nodes 2>&1 || true'"
23:20:09:16.690 NEW pid=28000 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd="C:\Program Files\Git\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
24:20:09:16.691 NEW pid=72884 parent=28000(?) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
25:20:09:16.691 NEW pid=22804 parent=28000(?) name=bash.exe cmd="C:\Program Files\Git\bin\..\usr\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
29:20:09:24.373 NEW pid=34400 parent=10396(?) name=conhost.exe cmd=
32:20:09:25.523 NEW pid=59160 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd="C:\Program Files\Git\bin\bash.exe" -c "$CLAUDE_PLUGIN_ROOT/owl.exe hook-check"
33:20:09:25.524 NEW pid=34176 parent=59160(?) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
34:20:09:26.209 NEW pid=70820 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd="C:\Program Files\Git\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
35:20:09:26.210 NEW pid=20348 parent=70820(bash) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
44:20:09:41.697 NEW pid=25200 parent=6384(?) name=conhost.exe cmd=
47:$ErrorActionPreference='Continue'; for ($i=1; $i -le 18; $i++) { $out = ssh david@gravity-linux 'bash -lc ''if [ -f ~/.spt-core/releases/release.json ]; then echo STAGED; ls -l ~/.spt-core/releases; cat ~/.spt-core/releases/release.json; else echo NOT_STAGED; fi; tail -40 ~/.spt-core/daemon-effects.log 2>/dev/null | grep -E \"UPDATE|update|STAGED|Rejected|pull\" || true'''; Write-Output \"--- poll $i ---\"; Write-Output $out; if ($out -match 'STAGED') { break }; Start-Sleep -Seconds 5 }"
48:20:09:46.681 NEW pid=74252 parent=55132(pwsh) name=ssh.exe cmd="C:\WINDOWS\system32\openssh\ssh.exe" david@gravity-linux "bash -lc 'if [ -f ~/.spt-core/releases/release.json ]; then echo STAGED; ls -l ~/.spt-core/releases; cat ~/.spt-core/releases/release.json; else echo NOT_STAGED; fi; tail -40 ~/.spt-core/daemon-effects.log 2>/dev/null | grep -E \"UPDATE|update|STAGED|Rejected|pull\" || true'"
51:20:09:55.121 NEW pid=25864 parent=38536(?) name=conhost.exe cmd=
55:$ErrorActionPreference='Continue'; for ($i=1; $i -le 24; $i++) { $out = ssh david@gravity-linux 'bash -lc ''if [ -f ~/.spt-core/releases/release.json ]; then echo STAGED; ls -l ~/.spt-core/releases; cat ~/.spt-core/releases/release.json; else echo NOT_STAGED; fi; tail -60 ~/.spt-core/daemon-effects.log 2>/dev/null | grep -E \"UPDATE|update|STAGED|Rejected|pull\" || true'''; Write-Output \"--- poll $i ---\"; Write-Output $out; if ($out -match '(?m)^STAGED$') { break }; Start-Sleep -Seconds 5 }"
56:20:09:56.485 NEW pid=72044 parent=55712(pwsh) name=ssh.exe cmd="C:\WINDOWS\system32\openssh\ssh.exe" david@gravity-linux "bash -lc 'if [ -f ~/.spt-core/releases/release.json ]; then echo STAGED; ls -l ~/.spt-core/releases; cat ~/.spt-core/releases/release.json; else echo NOT_STAGED; fi; tail -60 ~/.spt-core/daemon-effects.log 2>/dev/null | grep -E \"UPDATE|update|STAGED|Rejected|pull\" || true'"
58:20:09:56.715 NEW pid=73956 parent=67872(?) name=conhost.exe cmd=
60:20:09:57.511 NEW pid=49728 parent=73720(spt) name=git.exe cmd="git" -C C:\Users\decid\AppData\Local\spt-core\context\.seed.git for-each-ref --format=%(refname:short) refs/heads/
61:20:09:57.511 NEW pid=15656 parent=49728(?) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
63:20:09:57.780 NEW pid=74976 parent=73720(spt) name=git.exe cmd="git" -C C:\Users\decid\AppData\Local\spt-core\context\.seed.git rev-parse --verify --quiet refs/heads/a-lea
64:20:09:57.780 NEW pid=73784 parent=74976(git) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
65:20:09:58.158 NEW pid=70880 parent=41028(claude.exe.old.1780610438023) name=bash.exe cmd="C:\Program Files\Git\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
66:20:09:58.158 NEW pid=67960 parent=70880(?) name=conhost.exe cmd=\??\C:\WINDOWS\system32\conhost.exe 0x4
67:20:09:58.159 NEW pid=67272 parent=70880(?) name=bash.exe cmd="C:\Program Files\Git\bin\..\usr\bin\bash.exe" -c "node \"C:/Users/decid/.claude/hooks/gsd-statusline.js\""
70:$ErrorActionPreference='Continue'; ssh david@gravity-linux 'bash -lc ''echo before_version=$(spt --version); echo before_processes; ps -ef | grep spt | grep -v grep; timeout 60s spt update apply 2>&1; code=$?; echo apply_exit=$code; echo after_version=$(spt --version); echo after_processes; ps -ef | grep spt | grep -v grep; echo release_cache; ls -l ~/.spt-core/releases 2>/dev/null || true'''"
71:20:10:08.182 NEW pid=61644 parent=15792(pwsh) name=ssh.exe cmd="C:\WINDOWS\system32\openssh\ssh.exe" david@gravity-linux "bash -lc 'echo before_version=$(spt --version); echo before_processes; ps -ef | grep spt | grep -v grep; timeout 60s spt update apply 2>&1; code=$?; echo apply_exit=$code; echo after_version=$(spt --version); echo after_processes; ps -ef | grep spt | grep -v grep; echo release_cache; ls -l ~/.spt-core/releases 2>/dev/null || true'"


<task-notification>
<task-id>b58ptg41c</task-id>
<tool-use-id>toolu_01FvFEZRrmEbJyXVXM5iJqKX</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b58ptg41c.output</output-file>
<status>completed</status>
<summary>Background command "Watch new process spawns for 75 seconds" completed (exit code 0)</summary>
</task-notification>


310	            // not start a daemon that cannot serve (user-ratified
311	            // 2026-06-06: DEELEVATE_FAIL means spt won't function; refuse,
312	            // don't limp).
313	            Err(e) => {
314	                return Err(io::Error::other(format!(
315	                    "DEELEVATE_FAIL: {e} — refusing to start an ELEVATED daemon (it would deny \
316	                     unelevated clients); run spt from an unelevated shell"
317	                )));
318	            }
319	        }
320	        detached_no_inherit(&exe, &args)?;
321	        Ok(())
322	    }
323	    #[cfg(unix)]
324	    {
325	        use std::os::unix::process::CommandExt;
326	        let mut cmd = Command::new(exe);
327	        cmd.arg("daemon")
328	            .stdin(Stdio::null())
329	            .stdout(Stdio::null())
330	            .stderr(Stdio::null());
331	        if let Some(invoker) = crate::deelevate::sudo_invoker() {
332	            // Drop the child to the sudo invoker: std clears supplementary
333	            // groups and orders setgid-before-setuid when uid is set.
334	            invoker.stamp_env(&mut cmd);
335	            cmd.uid(invoker.uid).gid(invoker.gid);
336	        }
337	        // setsid: detach into a NEW SESSION, not just a new process group —
338	        // a daemon auto-started inside an ssh/non-interactive session
339	        // otherwise dies with it (sshd HUPs the whole session on
340	        // disconnect; observed live in the 2026-06-06 fleet deploy, where
341	        // the freshly-spawned daemon vanished the moment the ssh command
342	        // returned). `process_group(0)` is NOT enough — the new group
343	        // still belongs to the dying session. Safety: this closure only
344	        // calls the async-signal-safe setsid(2).
345	        unsafe {
346	            cmd.pre_exec(|| {
347	                if libc::setsid() == -1 {
348	                    return Err(std::io::Error::last_os_error());
349	                }

            // not start a daemon that cannot serve (user-ratified
            // 2026-06-06: DEELEVATE_FAIL means spt won't function; refuse,
            // don't limp).
            Err(e) => {
                return Err(io::Error::other(format!(
                    "DEELEVATE_FAIL: {e} — refusing to start an ELEVATED daemon (it would deny \
                     unelevated clients); run spt from an unelevated shell"
                )));
            }
        }
        detached_no_inherit(&exe, &args)?;
        Ok(())
    }
    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        let mut cmd = Command::new(exe);
        cmd.arg("daemon")
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null());
        if let Some(invoker) = crate::deelevate::sudo_invoker() {
            // Drop the child to the sudo invoker: std clears supplementary
            // groups and orders setgid-before-setuid when uid is set.
            invoker.stamp_env(&mut cmd);
            cmd.uid(invoker.uid).gid(invoker.gid);
        }
        // setsid: detach into a NEW SESSION, not just a new process group —
        // a daemon auto-started inside an ssh/non-interactive session
        // otherwise dies with it (sshd HUPs the whole session on
        // disconnect; observed live in the 2026-06-06 fleet deploy, where
        // the freshly-spawned daemon vanished the moment the ssh command
        // returned). `process_group(0)` is NOT enough — the new group
        // still belongs to the dying session. Safety: this closure only
        // calls the async-signal-safe setsid(2).
        unsafe {
            cmd.pre_exec(|| {
                if libc::setsid() == -1 {
                    return Err(std::io::Error::last_os_error());
                }


370	/// on a thread so a child that reads slowly never deadlocks the poll.
371	// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]
372	pub fn run_bounded_command(
373	    template: &str,
374	    keys: &BTreeMap<String, String>,
375	    timeout: Duration,
376	    stdin: Option<&[u8]>,
377	) -> Result<BoundedOutput, RuntimeError> {
378	    let filled = fill_template(template, keys)?;
379	    let tokens = tokenize(&filled);
380	    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
381	    let mut cmd = Command::new(program);
382	    cmd.args(args)
383	        .stdin(if stdin.is_some() {
384	            Stdio::piped()
385	        } else {
386	            Stdio::null()
387	        })
388	        .stdout(Stdio::piped())
389	        .stderr(Stdio::piped());
390	    let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;
391	    if let Some(bytes) = stdin {
392	        if let Some(mut sink) = child.stdin.take() {
393	            let owned = bytes.to_vec();
394	            // Write on a thread + drop the sink (EOF) so the child can finish.
395	            std::thread::spawn(move || {
396	                let _ = sink.write_all(&owned);
397	            });
398	        }
399	    }
400	    wait_bounded(child, timeout, &filled)
401	}
402
403	#[cfg(test)]
404	mod tests {
405	    use super::*;
406
407	    fn keys(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
408	        pairs
409	            .iter()

/// on a thread so a child that reads slowly never deadlocks the poll.
// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]
pub fn run_bounded_command(
    template: &str,
    keys: &BTreeMap<String, String>,
    timeout: Duration,
    stdin: Option<&[u8]>,
) -> Result<BoundedOutput, RuntimeError> {
    let filled = fill_template(template, keys)?;
    let tokens = tokenize(&filled);
    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
    let mut cmd = Command::new(program);
    cmd.args(args)
        .stdin(if stdin.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        })
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;
    if let Some(bytes) = stdin {
        if let Some(mut sink) = child.stdin.take() {
            let owned = bytes.to_vec();
            // Write on a thread + drop the sink (EOF) so the child can finish.
            std::thread::spawn(move || {
                let _ = sink.write_all(&owned);
            });
        }
    }
    wait_bounded(child, timeout, &filled)
}

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

    fn keys(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
        pairs
            .iter()


Prompt:
<task-notification>
<task-id>b58ptg41c</task-id>
<tool-use-id>toolu_01FvFEZRrmEbJyXVXM5iJqKX</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b58ptg41c.output</output-file>
<status>completed</status>
<summary>Background command "Watch new process spawns for 75 seconds" completed (exit code 0)</summary>
</task-notification>


55:- **Sister cite:** CHANGELOG v1.35.1 "IN-05"; `hook_subagent_start.rs:40-51`.
70:[Omitted long matching line]
165:### 5.1 Windows PID recycling false positives
171:### 5.2 Windows EBUSY on atomic rename
177:### 5.3 Git/subprocess timeout stamping
183:### 5.4 Windows UNC prefix in serialized paths
189:### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`
196:### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`
202:### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`
205:[Omitted long matching line]
217:- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.
229:- **Sister cite:** CHANGELOG v1.11.20 Phase 35.1.
294:| 2.1/5.1 | Stable PID/broker-handle over ephemeral PID | liveness detection |
304:| 5.2 | tmp-write + atomic-rename + retry (EBUSY) | all state writes, binary swap |
305:| 5.3 | Timeout every harness subprocess | manifest invocations |
306:| 5.4 | Strip UNC prefix on serialized paths | spt-proto path normalization |
307:| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |
308:| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |
309:| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |


552:id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
553-title = "Every harness/git subprocess has a timeout (5.3)"
554-required_stages = ["impl", "unit"]
555-[[requirements]]
556-id = "REQ-HAZARD-UNC-PATH-STRIP"
557-title = "Strip Windows UNC prefix on serialized paths (5.4)"
558-required_stages = ["impl", "unit"]  # activated M0: spt-store to_forward_slash (T8)
559-[[requirements]]
560-id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
--
585:id = "REQ-HAZARD-DETACHED-PIPE-INHERIT"
586-title = "Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)"
587-required_stages = ["impl", "unit"]  # activated M5-D3e (paid twice within one slice: std-strip guard wedged on a grandparent pipe): daemon::detached_no_inherit shared by spawn_detached + launch_shell; quoting unit + the bounded-capture spawn in shell_e2e.rs as regression
588-[[requirements]]
589-id = "REQ-HAZARD-CONPTY-DSR"
590-title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
591-required_stages = ["impl", "unit"]
592-[[requirements]]
593-id = "REQ-HAZARD-LOCAL-API-AUTH"


185	    resolve: impl FnOnce() -> Result<String, String>,
186	) {
187	    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
188	    let Some((program, args)) = tokens.split_first() else {
189	        eprintln!("WAKER_EMPTY:{owner}/{shell_id}: empty wake_command");
190	        return;
191	    };
192	    let mut failures = 0u32;
193	    while !stop.load(Ordering::SeqCst) {
194	        let mut cmd = Command::new(program);
195	        cmd.args(args)
196	            .stdin(Stdio::null())
197	            .stdout(Stdio::null())
198	            .stderr(Stdio::null());
199	        // The daemon (a console-less DETACHED process) hosting a
200	        // console-subsystem waker would otherwise pop a visible Terminal
201	        // window per spawn on Windows 11 (the KH 5.6-adjacent D3e lesson —
202	        // mock-shell's spt_cmd does the same).
203	        #[cfg(windows)]
204	        {
205	            use std::os::windows::process::CommandExt;
206	            cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
207	        }
208	        let child = cmd.spawn();
209	        let mut child = match child {
210	            Ok(c) => c,
211	            Err(e) => {
212	                // An unspawnable waker is a permanent condition — latch now.
213	                eprintln!("WAKER_SPAWN_FAIL:{owner}/{shell_id}: {e} (giving up)");
214	                let _ = std::fs::write(perch.join(WAKER_GAVE_UP_FILE), b"");
215	                return;
216	            }
217	        };
218	        let _ = std::fs::write(perch.join(WAKER_PID_FILE), child.id().to_string());
219	        let status = child.wait();

    resolve: impl FnOnce() -> Result<String, String>,
) {
    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
    let Some((program, args)) = tokens.split_first() else {
        eprintln!("WAKER_EMPTY:{owner}/{shell_id}: empty wake_command");
        return;
    };
    let mut failures = 0u32;
    while !stop.load(Ordering::SeqCst) {
        let mut cmd = Command::new(program);
        cmd.args(args)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null());
        // The daemon (a console-less DETACHED process) hosting a
        // console-subsystem waker would otherwise pop a visible Terminal
        // window per spawn on Windows 11 (the KH 5.6-adjacent D3e lesson —
        // mock-shell's spt_cmd does the same).
        #[cfg(windows)]
        {
            use std::os::windows::process::CommandExt;
            cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
        }
        let child = cmd.spawn();
        let mut child = match child {
            Ok(c) => c,
            Err(e) => {
                // An unspawnable waker is a permanent condition — latch now.
                eprintln!("WAKER_SPAWN_FAIL:{owner}/{shell_id}: {e} (giving up)");
                let _ = std::fs::write(perch.join(WAKER_GAVE_UP_FILE), b"");
                return;
            }
        };
        let _ = std::fs::write(perch.join(WAKER_PID_FILE), child.id().to_string());
        let status = child.wait();


189	### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`
190	- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.
191	- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.
192	- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).
193	- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.
194
195	<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->
196	### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`
197	- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for hours.
198	- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).
199	- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.
200	- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.
201
202	### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`
203	<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->
204	- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads "not running", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost rows).
205	- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs as-is with a loud warning — a consistent universe, never a torn one. Elevated one-shot *clients* talking to an unelevated daemon are fine (downward connects work); the daemon side is the invariant.
206	- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).
207	- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md "Non-admin daemon spawn"); interim field rule was "bring the daemon up unelevated FIRST".
208
209	---
210
211	## 6. Documented regressions (non-obvious invariants)
212
213	### 6.1 No flat/nested perch siblings; resolver-routed paths
214	- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.
215	- **Invariant:** one path resolver; never create divergent siblings.
216	- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.
217	- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.
218
219	### 6.2 Soft-cleanup preserves state, removes `ready`
220	- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.
221	- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.
222	- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.
223	- **Sister cite:** `src/owl/stop.rs`.
224
225	### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
226	- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.
227	- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.
228	- **spt-core mapping:** any destructive maintenance command must check for live child instances first.
229	- **Sister cite:** CHANGELOG v1.11.20 Phase 35.1.
230
231	### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
232	- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.
233	- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.

### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`
- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.
- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.
- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).
- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.

<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->
### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`
- **Failure:** a caller captures an `spt` invocation's output through a pipe (`Command::output()`, a harness hook reading the command). That `spt` process detach-spawns a **long-lived** child (the daemon via `ensure_running`; a shell binary via `spt shell spawn`). On Windows `CreateProcess` runs with `bInheritHandles = TRUE`, and the spt process's std handles — the caller's pipe write-ends — are inheritable by construction, so the immortal child inherits them even when its *own* stdio is `Stdio::null()`. The caller's pipe read never sees EOF: the capturing caller **hangs forever** (unix is immune — pipe fds are `CLOEXEC`). Paid twice: daemon spawn (guarded at D4a-era `spawn_detached`), then again at M5-D3e when the mock-shell E2E hung `spt shell spawn` for hours.
- **Invariant:** every detach-spawn of a long-lived child runs with **`bInheritHandles = FALSE`** (`spt-daemon::daemon::detached_no_inherit`) — zero handles flow, whatever the pipe's depth in the ancestry. Stripping `HANDLE_FLAG_INHERIT` from the spawner's *std* handles is NOT sufficient: a grandparent capture's pipe sits in the handle table as a stray inheritable handle and still flows through every `bInheritHandles = TRUE` hop (the first guard shipped that way and was wedged by exactly this — a daemon spawned three layers deep held the pwsh-level pipe of the CI/test harness).
- **spt-core mapping:** `spt-daemon::daemon::spawn_detached` (the daemon) and `spt-daemon::shellhost::launch_shell` (the relay-receipt shell binary). Any future long-lived detached spawn (manifest-template children included) must use the same no-inherit spawn.
- **Source:** spt-core, M5-D3e (`shell_e2e.rs` hang, 2026-06-04, twice — once per guard generation); Rust `Command` restricts *its own* created stdio handles but a parent's inheritable handle table still flows.

### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`
<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->
- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads "not running", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost rows).
- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs as-is with a loud warning — a consistent universe, never a torn one. Elevated one-shot *clients* talking to an unelevated daemon are fine (downward connects work); the daemon side is the invariant.
- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).
- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md "Non-admin daemon spawn"); interim field rule was "bring the daemon up unelevated FIRST".

---

## 6. Documented regressions (non-obvious invariants)

### 6.1 No flat/nested perch siblings; resolver-routed paths
- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.
- **Invariant:** one path resolver; never create divergent siblings.
- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.
- **Sister cite:** `src/common/perch_path.rs`; CHANGELOG Phase 25.4.

### 6.2 Soft-cleanup preserves state, removes `ready`
- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.
- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.
- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.
- **Sister cite:** `src/owl/stop.rs`.

### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children
- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.
- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.
- **spt-core mapping:** any destructive maintenance command must check for live child instances first.
- **Sister cite:** CHANGELOG v1.11.20 Phase 35.1.

### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind
- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.
- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.


585	id = "REQ-HAZARD-DETACHED-PIPE-INHERIT"
586	title = "Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)"
587	required_stages = ["impl", "unit"]  # activated M5-D3e (paid twice within one slice: std-strip guard wedged on a grandparent pipe): daemon::detached_no_inherit shared by spawn_detached + launch_shell; quoting unit + the bounded-capture spawn in shell_e2e.rs as regression
588	[[requirements]]
589	id = "REQ-HAZARD-CONPTY-DSR"
590	title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
591	required_stages = ["impl", "unit"]
592	[[requirements]]
593	id = "REQ-HAZARD-LOCAL-API-AUTH"
594	title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
595	required_stages = ["impl", "unit"]
596	[[requirements]]
597	id = "REQ-HAZARD-RESTART-IDEMPOTENT"
598	title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
599	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: tests/idempotent.rs crashes the brain before-intent/before-effect/after-effect at the PTY-write boundary → exactly-once (Spike #6)
600	[[requirements]]
601	id = "REQ-HAZARD-UPDATE-ROLLBACK"
602	title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
603	required_stages = ["impl", "unit"]  # activated M3c-C1: release::verify_metadata enforces monotonic version (rollback), metadata expiry, channel pinning, and key revocation. Adapter content signing is the C3 half (REQ-UPD-5/SEAM-UPDATE)
604	[[requirements]]
605	id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
606	title = "Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)"
607	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
608	# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
609	[[requirements]]

id = "REQ-HAZARD-DETACHED-PIPE-INHERIT"
title = "Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)"
required_stages = ["impl", "unit"]  # activated M5-D3e (paid twice within one slice: std-strip guard wedged on a grandparent pipe): daemon::detached_no_inherit shared by spawn_detached + launch_shell; quoting unit + the bounded-capture spawn in shell_e2e.rs as regression
[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-LOCAL-API-AUTH"
title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-RESTART-IDEMPOTENT"
title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: tests/idempotent.rs crashes the brain before-intent/before-effect/after-effect at the PTY-write boundary → exactly-once (Spike #6)
[[requirements]]
id = "REQ-HAZARD-UPDATE-ROLLBACK"
title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
required_stages = ["impl", "unit"]  # activated M3c-C1: release::verify_metadata enforces monotonic version (rollback), metadata expiry, channel pinning, and key revocation. Adapter content signing is the C3 half (REQ-UPD-5/SEAM-UPDATE)
[[requirements]]
id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
title = "Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
[[requirements]]


Edit

[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = ["impl", "unit"]

[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-CHILD-CONSOLE-FLASH"
title = "Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)"
required_stages = ["impl", "unit"]  # activated post-M7 field bug 2026-06-06: sync-pump git spawns flashed 2 windows/min on a Windows desktop; window-absence itself is untestable from a consoled test runner (child inherits the console) — the unit stage covers "flag does not break the spawn" (the error-87 regression class)


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)

[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-CHILD-CONSOLE-FLASH"
title = "Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)"
required_stages = ["impl", "unit"]  # activated post-M7 field bug 2026-06-06: sync-pump git spawns flashed 2 windows/min on a Windows desktop; window-absence itself is untestable from a consoled test runner (child inherits the console) — the unit stage covers "flag does not break the spawn" (the error-87 regression class)

+title = "Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)"

+required_stages = ["impl", "unit"]  # activated post-M7 field bug 2026-06-06: sync-pump git spawns flashed 2 windows/min on a Windows desktop; window-absence itself is untestable from a consoled test runner (child inherits the console) — the unit stage covers "flag does not break the spawn" (the error-87 regression class)

title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"


Now KNOWN-HAZARDS 5.8 + matrix row:


- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).
- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md "Non-admin daemon spawn"); interim field rule was "bring the daemon up unelevated FIRST".

---

- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).
- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md "Non-admin daemon spawn"); interim field rule was "bring the daemon up unelevated FIRST".

<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->
### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`
- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).
- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).
- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 "flag combo breaks spawn" regression class); window-absence was verified live by process-watch capture.
- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-daemon::shellwake` (already guarded).
- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).

---


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

- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).
- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md "Non-admin daemon spawn"); interim field rule was "bring the daemon up unelevated FIRST".

---

- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).
- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md "Non-admin daemon spawn"); interim field rule was "bring the daemon up unelevated FIRST".

<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->
### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`
- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).
- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).
- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 "flag combo breaks spawn" regression class); window-absence was verified live by process-watch capture.
- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-daemon::shellwake` (already guarded).
- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).

---

+### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`

+- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).

+- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).

+- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 "flag combo breaks spawn" regression class); window-absence was verified live by process-watch capture.

+- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-daemon::shellwake` (already guarded).

+- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).

## 6. Documented regressions (non-obvious invariants)


| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |

| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |
| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |


| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |
| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |

| 5.5 | ConPTY reader answers DSR (`ESC[6n`) | spt-term broker PTY reader |

| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |

+| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |

| 6.1 | Single path/registry source of truth | storage layout |

| 6.4 | Drop files supervisor-owned single-writer | runtime contract |

| 6.5 | Direct-write precedence marker (+ node id) | cross-node Psyche sync |


Now the three code fixes:


    if let Some(dir) = cwd {
        cmd.current_dir(dir);
    }
    let mut child = cmd.spawn()?;

    if let Some(dir) = cwd {
        cmd.current_dir(dir);
    }
    // The daemon is a console-less DETACHED process: without CREATE_NO_WINDOW
    // every git child pops a visible blank conhost window on the desktop
    // (KNOWN-HAZARDS 5.8 — the 60s sync pump flashed two windows per minute).
    // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
    }
    let mut child = cmd.spawn()?;


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

    if let Some(dir) = cwd {
        cmd.current_dir(dir);
    }
    // The daemon is a console-less DETACHED process: without CREATE_NO_WINDOW
    // every git child pops a visible blank conhost window on the desktop
    // (KNOWN-HAZARDS 5.8 — the 60s sync pump flashed two windows per minute).
    // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
    }
    let mut child = cmd.spawn()?;

//! Bounded `git` invocation — the one way spt-store talks to the system git
//! binary (BranchStore, ADR-0011; project-id derivation).
//!
//! Every call runs under a hard timeout: a hung git (network credential prompt,
//! dead filesystem, antivirus lock) is killed and reaped, never an indefinite
//! block (KNOWN-HAZARDS 5.3). Output pipes are drained on threads so a chatty
//! child can't deadlock the poll. Author/committer identity is pinned via env
//! on every call — BranchStore commits must never depend on (or pollute) the
//! user's global git config.
//!
//! Deliberately lives in spt-store (NOT reusing `spt-runtime`'s bounded runner):
//! spt-store sits *below* spt-runtime in the R-ARCH-1 layering, so the edge
//! would be a cycle. This is the minimal sibling, scoped to git argv (no shell,
//! no template substitution).
// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]

use std::io::{Read, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

/// Default budget for a local git plumbing call. Local-object operations are
/// milliseconds; the budget only has to beat "hung forever".
pub const GIT_TIMEOUT: Duration = Duration::from_secs(30);

/// A completed (non-timed-out) git invocation.
#[derive(Debug, Clone)]
pub struct GitOutput {
    pub status_code: Option<i32>,
    pub stdout: Vec<u8>,
    pub stderr: String,
}

impl GitOutput {
    pub fn success(&self) -> bool {
        self.status_code == Some(0)
    }

    /// stdout as trimmed UTF-8 (lossy) — the common porcelain-read shape.
    pub fn stdout_trimmed(&self) -> String {
        String::from_utf8_lossy(&self.stdout).trim().to_string()
    }
}

/// Run `git <args>` in `cwd` (when given) under `timeout`, optionally feeding
/// `stdin`. On timeout the child is killed + reaped and an `io::Error` of kind
/// `TimedOut` is returned — a hung git never blocks the caller indefinitely.
// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]
pub fn run_git(
    args: &[&str],
    cwd: Option<&Path>,
    stdin: Option<&[u8]>,
    timeout: Duration,
) -> std::io::Result<GitOutput> {
    let mut cmd = Command::new("git");
    cmd.args(args)
        // Pin identity + kill interactivity: no credential/ssh prompts can hang
        // us (they'd eat the whole timeout), no user-config bleed into commits.
        .env("GIT_AUTHOR_NAME", "spt-core")
        .env("GIT_AUTHOR_EMAIL", "spt-core@local")
        .env("GIT_COMMITTER_NAME", "spt-core")
        .env("GIT_COMMITTER_EMAIL", "spt-core@local")
        .env("GIT_TERMINAL_PROMPT", "0")
        .stdin(if stdin.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        })
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    if let Some(dir) = cwd {
        cmd.current_dir(dir);
    }
    let mut child = cmd.spawn()?;

    if let Some(bytes) = stdin {
        if let Some(mut sink) = child.stdin.take() {
            let owned = bytes.to_vec();
            // Write on a thread + drop the sink (EOF) so the child finishes.
            std::thread::spawn(move || {
                let _ = sink.write_all(&owned);
            });
        }
    }

    // Drain pipes on threads to avoid a full-pipe deadlock while polling.
    let mut out_pipe = child.stdout.take();
    let mut err_pipe = child.stderr.take();
    let out_handle = std::thread::spawn(move || {
        let mut buf = Vec::new();
        if let Some(p) = out_pipe.as_mut() {
            let _ = p.read_to_end(&mut buf);
        }
        buf
    });
    let err_handle = std::thread::spawn(move || {
        let mut buf = String::new();
        if let Some(p) = err_pipe.as_mut() {
            let _ = p.read_to_string(&mut buf);
        }
        buf
    });

    let start = Instant::now();
    let status = loop {
        match child.try_wait()? {
            Some(status) => break status,
            None => {
                if start.elapsed() >= timeout {
                    // [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT] hung git → kill + reap.
                    let _ = child.kill();
                    let _ = child.wait();
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::TimedOut,
                        format!(
                            "git {:?} timed out after {timeout:?}",
                            args.first().unwrap_or(&"")
                        ),
                    ));
                }
                std::thread::sleep(Duration::from_millis(10));
            }
        }
    };

    Ok(GitOutput {
        status_code: status.code(),
        stdout: out_handle.join().unwrap_or_default(),
        stderr: err_handle.join().unwrap_or_default(),
    })
}

/// [`run_git`] that promotes a non-zero exit to an `io::Error` carrying git's
/// stderr — the "this must work" shape BranchStore plumbing uses.
pub fn run_git_ok(
    args: &[&str],
    cwd: Option<&Path>,
    stdin: Option<&[u8]>,
) -> std::io::Result<GitOutput> {
    let out = run_git(args, cwd, stdin, GIT_TIMEOUT)?;
    if !out.success() {
        return Err(std::io::Error::other(format!(
            "git {} failed (exit {:?}): {}",
            args.join(" "),
            out.status_code,
            out.stderr.trim()
        )));
    }
    Ok(out)
}

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

    #[test] // [unit->REQ-HAZARD-SUBPROCESS-TIMEOUT] a completed git call reports output.
    fn version_runs_within_budget() {
        let out = run_git(&["--version"], None, None, GIT_TIMEOUT).expect("git on PATH");
        assert!(out.success());
        assert!(out.stdout_trimmed().contains("git version"));
    }

    #[test] // [unit->REQ-HAZARD-SUBPROCESS-TIMEOUT] the deadline path kills + reports TimedOut.
    fn deadline_kills_and_reports_timed_out() {
        // git has no portable "sleep" subcommand, so race a 0ms budget against
        // a real call: either the kill path fires (TimedOut) or the child won
        // the race on a fast machine — both are legitimate outcomes, and the
        // TimedOut arm is the one CI machines hit in practice.
        let res = run_git(&["--version"], None, None, Duration::from_millis(0));
        match res {
            Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::TimedOut),
            Ok(out) => assert!(out.success()),
        }
    }
}

+    // The daemon is a console-less DETACHED process: without CREATE_NO_WINDOW

+    // every git child pops a visible blank conhost window on the desktop

+    // (KNOWN-HAZARDS 5.8 — the 60s sync pump flashed two windows per minute).

+    // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]

+        use std::os::windows::process::CommandExt;

+        cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW


    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .args(["/PID", &pid.to_string(), "/T", "/F"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();

    #[cfg(windows)]
    {
        // taskkill is a console child of the console-less daemon: spawn it
        // windowless or teardown flashes a blank conhost (KNOWN-HAZARDS 5.8).
        // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]
        use std::os::windows::process::CommandExt;
        let _ = Command::new("taskkill")
            .args(["/PID", &pid.to_string(), "/T", "/F"])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .creation_flags(0x0800_0000) // CREATE_NO_WINDOW
            .status();
    }


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

    #[cfg(windows)]
    let _ = Command::new("taskkill")
        .args(["/PID", &pid.to_string(), "/T", "/F"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();

    #[cfg(windows)]
    {
        // taskkill is a console child of the console-less daemon: spawn it
        // windowless or teardown flashes a blank conhost (KNOWN-HAZARDS 5.8).
        // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]
        use std::os::windows::process::CommandExt;
        let _ = Command::new("taskkill")
            .args(["/PID", &pid.to_string(), "/T", "/F"])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .creation_flags(0x0800_0000) // CREATE_NO_WINDOW
            .status();
    }

+        // taskkill is a console child of the console-less daemon: spawn it

+        // windowless or teardown flashes a blank conhost (KNOWN-HAZARDS 5.8).

+        // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]

+        let _ = Command::new("taskkill")

+            .creation_flags(0x0800_0000) // CREATE_NO_WINDOW


    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
    let mut cmd = Command::new(program);
    cmd.args(args)
        .stdin(if stdin.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        })
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;

    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
    let mut cmd = Command::new(program);
    cmd.args(args)
        .stdin(if stdin.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        })
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    // Bounded commands run from the console-less daemon (notif pump, manifest
    // hooks): without CREATE_NO_WINDOW each spawn flashes a visible blank
    // conhost window on the desktop (KNOWN-HAZARDS 5.8).
    // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
    }
    let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;


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

    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
    let mut cmd = Command::new(program);
    cmd.args(args)
        .stdin(if stdin.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        })
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;

    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
    let mut cmd = Command::new(program);
    cmd.args(args)
        .stdin(if stdin.is_some() {
            Stdio::piped()
        } else {
            Stdio::null()
        })
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    // Bounded commands run from the console-less daemon (notif pump, manifest
    // hooks): without CREATE_NO_WINDOW each spawn flashes a visible blank
    // conhost window on the desktop (KNOWN-HAZARDS 5.8).
    // [impl->REQ-HAZARD-CHILD-CONSOLE-FLASH]
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
    }
    let mut child = cmd.spawn().map_err(RuntimeError::Spawn)?;

+    // Bounded commands run from the console-less daemon (notif pump, manifest

+    // hooks): without CREATE_NO_WINDOW each spawn flashes a visible blank

+    // conhost window on the desktop (KNOWN-HAZARDS 5.8).

if let Some(mut sink) = child.stdin.take() {


Unit tags — gitrun test exercises flagged spawn:


    #[test] // [unit->REQ-HAZARD-SUBPROCESS-TIMEOUT] a completed git call reports output.
    fn version_runs_within_budget() {

    #[test] // [unit->REQ-HAZARD-SUBPROCESS-TIMEOUT] a completed git call reports output.
    // [unit->REQ-HAZARD-CHILD-CONSOLE-FLASH] the CREATE_NO_WINDOW spawn still
    // works end-to-end (window-absence itself is unobservable from a consoled
    // test runner — see KNOWN-HAZARDS 5.8; the live watcher verified that half).
    fn version_runs_within_budget() {