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

//! B5 regression — exactly-once side effects across a brain crash, on the real
//! PTY-write boundary over the versioned IPC (KNOWN-HAZARDS 7.2,
//! [`REQ-HAZARD-RESTART-IDEMPOTENT`]; Spike #6 made real).
//!
//! A durable source of ordered input ops (each a unique line) is delivered to a
//! single echo child the broker hosts for the whole test. The brain is crashed
//! (its connection dropped) at each of the three dangerous windows around an
//! effect, and a fresh brain re-attaches and re-drives the source from the
//! start. The broker — the surviving party, holding the exactly-once anchor —
//! must apply every op **exactly once**: the child echoes each line once and the
//! broker's applied-set holds every op once, with no dup and no drop, across the
//! whole gauntlet.
//!
//! Crash windows, per the acceptance criterion:
//!   - **before-intent** — the brain dies before issuing the op (no commitment;
//!     the re-drive from the durable source is what prevents a drop).
//!   - **before-effect** — the brain issues the op (frame on the wire) then dies
//!     before learning it landed; the broker applies it, the ack is lost.
//!   - **after-effect** — the brain sees the ack (the effect is durably applied)
//!     then dies before retiring the op from its source; the re-drive is deduped.
//!
//! Because the broker dedups by durable id, *which* life applies a given op is
//! irrelevant — the end state is exactly-once regardless. A real-process SIGKILL
//! of the brain was proven in Spike #1/#6; here the brain is a library driven
//! in-thread and a dropped connection is the crash boundary (the broker + child
//! survive; the anchor persists).
// [int->REQ-HAZARD-RESTART-IDEMPOTENT]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use spt_daemon::brain::{Brain, BrainState, BrokerEvent};
use spt_daemon::msg::SpawnReq;
use spt_daemon::Broker;

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique_name() -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-b5-{}-{}.sock", std::process::id(), n)
}

fn echo_spawn_req() -> SpawnReq {
    #[cfg(unix)]
    let (program, args) = ("cat".to_string(), Vec::<String>::new());
    #[cfg(windows)]
    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: String::new(),
    }
}

fn cold_retry(name: &str, now_ms: u64) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, now_ms) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain cold-start could not connect");
}

fn handoff_retry(name: &str, prior: BrainState, now_ms: u64) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::handoff(name, prior.clone(), now_ms) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain handoff could not connect");
}

#[derive(Clone, Copy, PartialEq)]
enum Window {
    BeforeIntent,
    BeforeEffect,
    AfterEffect,
}

/// The line for op `n` — engineered so no line is a substring of another
/// (`OP1_END` is not inside `OP10_END`), so an occurrence count is exact.
fn line_for(op: u64) -> String {
    format!("OP{op}_END\r")
}

fn needle_for(op: u64) -> String {
    format!("OP{op}_END")
}

/// Count non-overlapping occurrences of `needle` in `hay`.
fn count(hay: &[u8], needle: &[u8]) -> usize {
    if needle.is_empty() || hay.len() < needle.len() {
        return 0;
    }
    let mut n = 0;
    let mut i = 0;
    while i + needle.len() <= hay.len() {
        if &hay[i..i + needle.len()] == needle {
            n += 1;
            i += needle.len();
        } else {
            i += 1;
        }
    }
    n
}

/// Drive one brain life: re-send the durable source `0..total` in order, draining
/// output (and acks) into `buf`, injecting `crash` on one op. Returns the
/// snapshot to hand to the next life. `buf` is shared across lives, so each
/// gaplessly-resumed output chunk is accumulated exactly once.
fn run_life(brain: &mut Brain, buf: &mut Vec<u8>, total: u64, crash: Option<(u64, Window)>) {
    for op in 0..total {
        if crash == Some((op, Window::BeforeIntent)) {
            return;
        }
        brain
            .send_effect(op, line_for(op).as_bytes())
            .expect("send_effect");
        if crash == Some((op, Window::BeforeEffect)) {
            return; // issued, but die before learning it landed
        }
        // Read until the ack for this op arrives, draining interleaved output.
        loop {
            match brain.read_event().expect("read event") {
                BrokerEvent::Applied { op_id, .. } if op_id == op => break,
                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),
                BrokerEvent::Exit { .. } => panic!("child exited mid-delivery"),
                BrokerEvent::Error { message } => panic!("broker error: {message}"),
                _ => {}
            }
        }
        if crash == Some((op, Window::AfterEffect)) {
            return; // ack seen; die before retiring the op from the source
        }
    }
}

#[test]
fn pty_writes_are_exactly_once_across_brain_crashes() {
    const TOTAL: u64 = 8;
    let name = unique_name();
    let journal = tempfile::tempdir().expect("tempdir");
    let journal_path = journal.path().join("effects.log");
    // The broker-owned anchor points at a temp file (not the shared home).
    let broker = Broker::bind_in(&name, journal_path).expect("bind broker");

    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });

    // Generation 0: cold start + spawn the long-lived echo child.
    let mut now = 5_000_000u64;
    let mut brain = cold_retry(&name, now);
    let sid = brain
        .spawn_session(echo_spawn_req())
        .expect("spawn session");
    let child_pid = broker.session_pid(sid);

    let mut buf: Vec<u8> = Vec::new();

    // A gauntlet hitting every window, including repeats around the same op and
    // the no-dup trap (an op acked then re-driven).
    let gauntlet = [
        Some((1, Window::BeforeIntent)),
        Some((3, Window::BeforeEffect)), // applied at the broker, ack lost
        Some((3, Window::BeforeIntent)), // re-drive dies again before reaching 3
        Some((5, Window::AfterEffect)),  // acked, then crash before retiring
        Some((6, Window::BeforeEffect)),
        None, // a clean life delivers the tail
    ];

    let mut state = brain.snapshot();
    drop(brain); // first crash

    for crash in gauntlet {
        now += 1;
        let mut brain = handoff_retry(&name, state.clone(), now);
        // The child + its PTY survive every restart untouched.
        assert_eq!(
            broker.session_count(),
            1,
            "exactly one session across restarts"
        );
        assert_eq!(
            broker.session_pid(sid),
            child_pid,
            "child pid stable across restart"
        );

        run_life(&mut brain, &mut buf, TOTAL, crash);
        state = brain.snapshot();
        drop(brain);
    }

    // Drain any output the final clean life produced but had not yet read back
    // (the child echoes asynchronously): re-attach once more and pull remaining
    // output until every line is present.
    now += 1;
    let mut brain = handoff_retry(&name, state, now);
    for _ in 0..2000 {
        if (0..TOTAL).all(|op| count(&buf, needle_for(op).as_bytes()) >= 1) {
            break;
        }
        match brain.read_event() {
            Ok(BrokerEvent::Output { bytes, .. }) => buf.extend_from_slice(&bytes),
            Ok(_) => {}
            Err(_) => break,
        }
    }
    drop(brain);

    // ---- Exactly-once, value-set verified --------------------------------
    //
    // The broker-owned anchor is the *authoritative* exactly-once evidence:
    // `apply_once` is the only path that writes the PTY for an `op_id`, it runs
    // the write exactly once per key (subsequent deliveries are deduped), and it
    // records the key only after the write succeeds. So `applied_count == TOTAL`
    // with every key applied ⇒ every PTY write ran exactly once — NO DUP (a
    // replay was deduped) and NO DROP (a re-drive re-attempted any un-applied op),
    // across all three crash windows.
    assert_eq!(
        broker.journal().applied_count() as u64,
        TOTAL,
        "applied-set holds every op exactly once (no dup, no drop)"
    );
    for op in 0..TOTAL {
        assert!(
            broker.journal().is_applied((sid, op)),
            "op {op} recorded applied"
        );
    }
    // Corroboration that the writes truly reached the live PTY (not just the
    // anchor): every line streamed back. An exact echo *count* is intentionally
    // not asserted — a PTY in cooked mode echoes typed input and the echo child
    // emits it again, so each line legitimately appears more than once; the
    // anchor above, not the byte count, is what proves single-application.
    for op in 0..TOTAL {
        assert!(
            count(&buf, needle_for(op).as_bytes()) >= 1,
            "line for op {op} must have reached the PTY and streamed back\nbuf={:?}",
            String::from_utf8_lossy(&buf)
        );
    }
}


proceed with W1.5 next steps. first verify the plan with doyle


Edit

        endpoint: String::new(),
    }
}

        endpoint: String::new(),
        cwd: None,
    }
}


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

        endpoint: String::new(),
    }
}

        endpoint: String::new(),
        cwd: None,
    }
}

rows: 24,
cols: 80,
endpoint: String::new(),
+        cwd: None,
}
}


            endpoint: format!("budget-{i}"),
        };

            endpoint: format!("budget-{i}"),
            cwd: None,
        };


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

            endpoint: format!("budget-{i}"),
        };

            endpoint: format!("budget-{i}"),
            cwd: None,
        };

//! Dormancy resource-budget measurement (D9-3, ADR-0003 red-team #9).
//!
//! **Env-gated: runs only with `SPT_BUDGET=1`** — CI never sets it, so this
//! is a silent no-op there; the real runs happen manually on the rig hosts
//! (HFENDULEAM Windows + kitsubito Linux) and their output is recorded in
//! `docs/DORMANCY-BUDGET.md`. Not a pass/fail test: it MEASURES the warm
//! (dormant) seat cost so the warm/cold default is a number-backed policy
//! instead of a guess (the #9 gate on REQ-INST-3's `doc` stage).
//!
//! ## What it measures
//!
//! A **dormant** seat is a live, state-preserved harness session held warm
//! by the broker (PTY master + child process + output ring). A **suspended**
//! seat is that session closed — only the on-disk perch record remains. So:
//!
//! 1. spawn N idle sessions through the real broker (the production PTY
//!    hosting path), labeled per seat;
//! 2. settle, then sample each child's RSS / handle (fd) count / CPU time,
//!    twice over an idle window (the delta = idle CPU burn);
//! 3. also sample THIS process's RSS before/after spawning (the broker-side
//!    per-seat overhead: PTY plumbing + output rings);
//! 4. kill the sessions (the suspended state) and sample the residual.
//!
//! The per-seat child cost + broker delta IS the warm premium over
//! suspended; the suspended residual is ~the daemon baseline.
//!
//! ## Knobs (env)
//!
//! - `SPT_BUDGET_N` — seats to spawn (default 5).
//! - `SPT_BUDGET_PROGRAM` / `SPT_BUDGET_ARGS` — the harness command per seat
//!   (space-split args). Default: the platform shell idling at a prompt. On
//!   a rig run, point it at the real adapter's harness binary (e.g.
//!   `claude`) — the "real adapter" #9 asks for.
//! - `SPT_BUDGET_SETTLE_MS` — post-spawn settle before the first sample
//!   (default 3000; raise for a heavy harness that loads slowly).
//! - `SPT_BUDGET_IDLE_MS` — the idle CPU window between samples (default 5000).
// [impl->REQ-INST-3]

use std::sync::Arc;
use std::thread;
use std::time::Duration;

use spt_daemon::brain::Brain;
use spt_daemon::broker::Broker;
use spt_daemon::msg::SpawnReq;
use tempfile::TempDir;

/// One sampled process snapshot.
#[derive(Debug, Clone, Copy, Default)]
struct Sample {
    rss_bytes: u64,
    handles: u64,
    cpu_seconds: f64,
}

#[test]
fn measure_dormant_seat_budget() {
    if std::env::var("SPT_BUDGET").as_deref() != Ok("1") {
        return; // silent skip — the rig runs this, never CI
    }

    let n: usize = env_or("SPT_BUDGET_N", 5);
    let settle = Duration::from_millis(env_or("SPT_BUDGET_SETTLE_MS", 3_000));
    let idle = Duration::from_millis(env_or("SPT_BUDGET_IDLE_MS", 5_000));
    let (program, args) = budget_command();

    // Hermetic home + a real (net-less) broker — the production PTY host.
    let home = TempDir::new().expect("temp home");
    std::env::set_var("SPT_HOME", home.path());
    let name = format!("spt-budget-{}.sock", std::process::id());
    let broker = Broker::bind_in_with_net(&name, home.path().join("effects.log"), None)
        .expect("bind broker");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });

    let self_before = sample_pid(std::process::id()).unwrap_or_default();

    // One probe brain per seat (kill_session targets the brain's own session).
    let mut seats: Vec<(Brain, u64, u32)> = Vec::new(); // (brain, session, child pid)
    for i in 0..n {
        let mut brain = connect_retry(&name);
        let req = SpawnReq {
            program: program.clone(),
            args: args.clone(),
            rows: 24,
            cols: 80,
            endpoint: format!("budget-{i}"),
        };
        let (session, pid) = brain.spawn_session_pid(req).expect("spawn seat");
        let pid = pid.expect("seat child pid");
        seats.push((brain, session, pid));
    }

    thread::sleep(settle);
    let first: Vec<Sample> = seats
        .iter()
        .map(|(_, _, pid)| sample_pid(*pid).unwrap_or_default())
        .collect();
    let self_warm = sample_pid(std::process::id()).unwrap_or_default();

    thread::sleep(idle);
    let second: Vec<Sample> = seats
        .iter()
        .map(|(_, _, pid)| sample_pid(*pid).unwrap_or_default())
        .collect();

    // The measurement report — the runbook captures this stdout block into
    // docs/DORMANCY-BUDGET.md verbatim.
    println!(
        "## dormancy budget — {} x `{program} {}`",
        n,
        args.join(" ")
    );
    println!("host: {} ({})", hostname(), std::env::consts::OS);
    println!();
    println!(
        "| seat | pid | rss | handles/fds | idle cpu (s over {:?}) |",
        idle
    );
    println!("|------|-----|-----|-------------|------------------------|");
    let (mut rss_total, mut handle_total, mut cpu_total) = (0u64, 0u64, 0f64);
    for (i, (a, b)) in first.iter().zip(&second).enumerate() {
        let burn = (b.cpu_seconds - a.cpu_seconds).max(0.0);
        rss_total += b.rss_bytes;
        handle_total += b.handles;
        cpu_total += burn;
        println!(
            "| budget-{i} | {} | {} | {} | {burn:.3} |",
            seats[i].2,
            fmt_bytes(b.rss_bytes),
            b.handles
        );
    }
    println!();
    println!(
        "per-seat avg: rss {} · {} handles/fds · idle cpu {:.3} s/{:?}",
        fmt_bytes(rss_total / n as u64),
        handle_total / n as u64,
        cpu_total / n as f64,
        idle
    );
    println!(
        "broker-side overhead for {n} seats (this process RSS delta): {}",
        fmt_bytes(self_warm.rss_bytes.saturating_sub(self_before.rss_bytes))
    );

    // Suspend: close every seat (the session ends; only the perch record
    // would remain in production). The residual is the daemon baseline.
    for (brain, _, _) in seats.iter_mut() {
        let _ = brain.kill_session();
    }
    thread::sleep(Duration::from_millis(1_500));
    let self_after = sample_pid(std::process::id()).unwrap_or_default();
    println!(
        "suspended residual (this process RSS after killing all seats): {} (was {} warm)",
        fmt_bytes(self_after.rss_bytes),
        fmt_bytes(self_warm.rss_bytes)
    );
}

fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
    std::env::var(key)
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(default)
}

/// The per-seat harness command: `SPT_BUDGET_PROGRAM`/`SPT_BUDGET_ARGS`, or
/// the platform shell idling at a prompt.
fn budget_command() -> (String, Vec<String>) {
    let program = std::env::var("SPT_BUDGET_PROGRAM").unwrap_or_else(|_| {
        if cfg!(windows) {
            "cmd".to_string()
        } else {
            "sh".to_string()
        }
    });
    let args = std::env::var("SPT_BUDGET_ARGS")
        .map(|s| s.split_whitespace().map(str::to_string).collect())
        .unwrap_or_default();
    (program, args)
}

fn hostname() -> String {
    std::env::var("COMPUTERNAME")
        .or_else(|_| std::env::var("HOSTNAME"))
        .unwrap_or_else(|_| "unknown".to_string())
}

fn fmt_bytes(b: u64) -> String {
    if b >= 1 << 20 {
        format!("{:.1} MiB", b as f64 / (1 << 20) as f64)
    } else {
        format!("{:.1} KiB", b as f64 / 1024.0)
    }
}

fn connect_retry(name: &str) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, 1) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain could not connect");
}

/// Sample one process: RSS bytes, handle/fd count, cumulative CPU seconds.
/// Shelling out keeps the harness dependency-free — measurement cadence is
/// seconds, not microseconds.
#[cfg(windows)]
fn sample_pid(pid: u32) -> Option<Sample> {
    let out = std::process::Command::new("powershell")
        .args([
            "-NoProfile",
            "-Command",
            &format!(
                "$p = Get-Process -Id {pid} -ErrorAction Stop; \
                 Write-Output \"$($p.WorkingSet64)|$($p.HandleCount)|$($p.TotalProcessorTime.TotalSeconds)\""
            ),
        ])
        .output()
        .ok()?;
    let text = String::from_utf8_lossy(&out.stdout);
    let mut parts = text.trim().split('|');
    Some(Sample {
        rss_bytes: parts.next()?.parse().ok()?,
        handles: parts.next()?.parse().ok()?,
        cpu_seconds: parts.next()?.parse().ok()?,
    })
}

#[cfg(unix)]
fn sample_pid(pid: u32) -> Option<Sample> {
    // VmRSS from /proc/<pid>/status (kB).
    let status = std::fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
    let rss_kb: u64 = status
        .lines()
        .find(|l| l.starts_with("VmRSS:"))?
        .split_whitespace()
        .nth(1)?
        .parse()
        .ok()?;
    // Open fd count.
    let fds = std::fs::read_dir(format!("/proc/{pid}/fd"))
        .map(|d| d.count() as u64)
        .unwrap_or(0);
    // utime+stime (clock ticks) from /proc/<pid>/stat — fields 14+15, counted
    // after the parenthesized comm (which may contain spaces).
    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
    let after_comm = stat.rsplit_once(')')?.1;
    let fields: Vec<&str> = after_comm.split_whitespace().collect();
    let utime: u64 = fields.get(11)?.parse().ok()?; // field 14 overall
    let stime: u64 = fields.get(12)?.parse().ok()?; // field 15 overall
    let hz = 100.0; // USER_HZ on every Linux the rig runs
    Some(Sample {
        rss_bytes: rss_kb * 1024,
        handles: fds,
        cpu_seconds: (utime + stime) as f64 / hz,
    })
}


        endpoint: endpoint.to_string(),
    }

        endpoint: endpoint.to_string(),
        cwd: None,
    }


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

        endpoint: endpoint.to_string(),
    }

        endpoint: endpoint.to_string(),
        cwd: None,
    }


            endpoint: "ling-drive".to_string(),
        })

            endpoint: "ling-drive".to_string(),
            cwd: None,
        })


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

            endpoint: "ling-drive".to_string(),
        })

            endpoint: "ling-drive".to_string(),
            cwd: None,
        })


            endpoint: String::new(),
        })

            endpoint: String::new(),
            cwd: None,
        })


            endpoint: String::new(),
        })

            endpoint: String::new(),
            cwd: None,
        })

rows: 24,
cols: 80,
endpoint: String::new(),
+            cwd: None,
})
.expect("spawn unlabeled");


            endpoint: ID_B.to_string(),
        })

            endpoint: ID_B.to_string(),
            cwd: None,
        })


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

            endpoint: ID_B.to_string(),
        })

            endpoint: ID_B.to_string(),
            cwd: None,
        })

// Publish the session id where A's file-fetch rung reads it (the fetch IS


All literals patched. Now write the REQ-HOST-RUN-2 tests. Check spt-term pty.rs test module + drain pattern:


14://! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup
23:use crate::surface::{SessionSurface, SurfaceError, SurfaceSize};
25:/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain
32:/// (or the A3 `send_keys`/`send_line` helpers), drain output via
33:/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /
38:    /// The single PTY writer, shared so the A2 drain thread can answer DSR.
45:    /// The master reader, taken at most once (`take_reader`).
57:    /// behaviour is governed by the writer, handled by the A2 drain discipline.)
60:    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {
96:    pub fn spawn_program<P, A, S>(
99:        size: SurfaceSize,
114:    pub fn spawn_program_in<P, A, S, D>(
117:        size: SurfaceSize,
137:    /// second call — output is drained by exactly one consumer (the A2 drain
139:    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {
147:    /// The shared writer handle — cloned to the A2 DSR drain thread so its
165:        self.child.lock().unwrap().wait().map_err(SurfaceError::Io)
203:    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {


crates\spt-term\src\reader.rs
crates\spt-term\src\stream.rs
crates\spt-term\src\surface.rs
crates\spt-term\tests\dsr.rs
crates\spt-term\tests\inject.rs
crates\spt-term\tests\stream.rs
crates\spt-term\tests\surface.rs
crates\spt-term\src\digest.rs
crates\spt-term\src\lib.rs
crates\spt-term\src\projection.rs
crates\spt-term\src\pty.rs


1	//! A1 regression tests for the native-PTY [`SessionSurface`] backend.
2	//!
3	//! Two guarantees:
4	//!  - **resize-under-load** does not leak/hang/panic (Spike #5) — runs on both
5	//!    OSes; the master lock must serialize a resize storm against itself.
6	//!  - **output is captured** off a real PTY — Unix-gated here (the `forkpty` raw
7	//!    pipe EOFs cleanly); the Windows/ConPTY capture path needs the A2 DSR
8	//!    auto-answer and is proven by that task's regression test.
9
10	use std::sync::Arc;
11	use std::thread;
12
13	use spt_term::{PtySession, SessionSurface, SurfaceSize};
14
15	/// A long-lived child, OS-appropriate, that stays alive while we hammer it.
16	fn long_lived() -> PtySession {
17	    #[cfg(unix)]
18	    let s = PtySession::spawn_program("sleep", ["30"], SurfaceSize::new(24, 80));
19	    #[cfg(windows)]
20	    let s = PtySession::spawn_program("ping", ["-n", "30", "127.0.0.1"], SurfaceSize::new(24, 80));
21	    s.expect("spawn long-lived child under a pty")
22	}
23
24	// [unit->REQ-TERM-1]
25	#[test]
26	fn resize_under_load_does_not_hang_or_panic() {
27	    let session = Arc::new(long_lived());
28	    assert!(session.process_id().is_some(), "a real PTY child has a pid");
29
30	    // Three concurrent resizers contend the master lock: the storm must
31	    // serialize cleanly (no panic, no error, no deadlock).
32	    let mut handles = Vec::new();
33	    for t in 0..3u16 {
34	        let s = Arc::clone(&session);
35	        handles.push(thread::spawn(move || {
36	            for i in 0..150u16 {
37	                let rows = 10 + ((t * 7 + i) % 40);
38	                let cols = 40 + ((t * 13 + i) % 80);
39	                s.resize(SurfaceSize::new(rows, cols))
40	                    .expect("resize under contention must not fail");
41	            }
42	        }));
43	    }
44	    for h in handles {
45	        h.join().expect("resizer thread must not panic");
46	    }
47
48	    // Surface still usable after the storm; tear the child down cleanly.
49	    session
50	        .resize(SurfaceSize::new(24, 80))
51	        .expect("post-storm resize");
52	    session.kill().expect("kill child");
53	    let _ = session.wait().expect("reap child after kill");
54	}
55
56	// [unit->REQ-TERM-1]
57	#[cfg(unix)]
58	#[test]
59	fn captures_child_output_off_the_pty() {
60	    use std::io::Read;
61
62	    let session = PtySession::spawn_program(
63	        "sh",
64	        ["-c", "printf 'hello-surface\\n'"],
65	        SurfaceSize::new(24, 80),
66	    )
67	    .expect("spawn echo child");
68
69	    let mut reader = session.take_reader().expect("take the master reader");
70	    // Second take is rejected — output has exactly one consumer.
71	    assert!(session.take_reader().is_err(), "reader is once-only");
72
73	    let mut out = Vec::new();
74	    reader
75	        .read_to_end(&mut out)
76	        .expect("read child output to EOF");
77	    let text = String::from_utf8_lossy(&out);
78	    assert!(
79	        text.contains("hello-surface"),
80	        "expected child stdout in the captured stream, got: {text:?}"
81	    );
82
83	    let _ = session.wait().expect("reap child");
84	}
85

//! A1 regression tests for the native-PTY [`SessionSurface`] backend.
//!
//! Two guarantees:
//!  - **resize-under-load** does not leak/hang/panic (Spike #5) — runs on both
//!    OSes; the master lock must serialize a resize storm against itself.
//!  - **output is captured** off a real PTY — Unix-gated here (the `forkpty` raw
//!    pipe EOFs cleanly); the Windows/ConPTY capture path needs the A2 DSR
//!    auto-answer and is proven by that task's regression test.

use std::sync::Arc;
use std::thread;

use spt_term::{PtySession, SessionSurface, SurfaceSize};

/// A long-lived child, OS-appropriate, that stays alive while we hammer it.
fn long_lived() -> PtySession {
    #[cfg(unix)]
    let s = PtySession::spawn_program("sleep", ["30"], SurfaceSize::new(24, 80));
    #[cfg(windows)]
    let s = PtySession::spawn_program("ping", ["-n", "30", "127.0.0.1"], SurfaceSize::new(24, 80));
    s.expect("spawn long-lived child under a pty")
}

// [unit->REQ-TERM-1]
#[test]
fn resize_under_load_does_not_hang_or_panic() {
    let session = Arc::new(long_lived());
    assert!(session.process_id().is_some(), "a real PTY child has a pid");

    // Three concurrent resizers contend the master lock: the storm must
    // serialize cleanly (no panic, no error, no deadlock).
    let mut handles = Vec::new();
    for t in 0..3u16 {
        let s = Arc::clone(&session);
        handles.push(thread::spawn(move || {
            for i in 0..150u16 {
                let rows = 10 + ((t * 7 + i) % 40);
                let cols = 40 + ((t * 13 + i) % 80);
                s.resize(SurfaceSize::new(rows, cols))
                    .expect("resize under contention must not fail");
            }
        }));
    }
    for h in handles {
        h.join().expect("resizer thread must not panic");
    }

    // Surface still usable after the storm; tear the child down cleanly.
    session
        .resize(SurfaceSize::new(24, 80))
        .expect("post-storm resize");
    session.kill().expect("kill child");
    let _ = session.wait().expect("reap child after kill");
}

// [unit->REQ-TERM-1]
#[cfg(unix)]
#[test]
fn captures_child_output_off_the_pty() {
    use std::io::Read;

    let session = PtySession::spawn_program(
        "sh",
        ["-c", "printf 'hello-surface\\n'"],
        SurfaceSize::new(24, 80),
    )
    .expect("spawn echo child");

    let mut reader = session.take_reader().expect("take the master reader");
    // Second take is rejected — output has exactly one consumer.
    assert!(session.take_reader().is_err(), "reader is once-only");

    let mut out = Vec::new();
    reader
        .read_to_end(&mut out)
        .expect("read child output to EOF");
    let text = String::from_utf8_lossy(&out);
    assert!(
        text.contains("hello-surface"),
        "expected child stdout in the captured stream, got: {text:?}"
    );

    let _ = session.wait().expect("reap child");
}


Good template. Check spt-term dev-deps for tempfile:


Bash

grep -nA8 "dev-dependencies" "C:\Users\decid\Documents\projects\spt-core\crates\spt-term\Cargo.toml"

spt-term dev-deps


20:[dev-dependencies]
21-tempfile = "3"


Edit

    let _ = session.wait().expect("reap child");
}

    let _ = session.wait().expect("reap child");
}

// [unit->REQ-HOST-RUN-2] `spawn_program_in` lands the child in the requested
// working directory: a child that prints its cwd reports the dir we passed.
// Asserted on the unique tempdir leaf (robust against /var↔/private/var and
// logical-vs-physical pwd differences — the random component is identical
// either way). Unix-gated like the capture test (clean raw-pipe EOF).
#[cfg(unix)]
#[test]
fn spawn_program_in_lands_the_child_in_the_requested_cwd() {
    use std::io::Read;

    let dir = tempfile::tempdir().expect("tempdir");
    let leaf = dir
        .path()
        .file_name()
        .expect("tempdir has a final component")
        .to_string_lossy()
        .into_owned();

    let session = PtySession::spawn_program_in(
        "sh",
        ["-c", "pwd"],
        SurfaceSize::new(24, 80),
        Some(dir.path()),
    )
    .expect("spawn pwd child in the requested cwd");

    let mut reader = session.take_reader().expect("take reader");
    let mut out = Vec::new();
    reader.read_to_end(&mut out).expect("read child cwd to EOF");
    let text = String::from_utf8_lossy(&out);
    assert!(
        text.contains(&leaf),
        "child's pwd must be the requested cwd (leaf {leaf:?}), got: {text:?}"
    );

    let _ = session.wait().expect("reap child");
}


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

    let _ = session.wait().expect("reap child");
}

// [unit->REQ-HOST-RUN-2] `spawn_program_in` lands the child in the requested
// working directory: a child that prints its cwd reports the dir we passed.
// Asserted on the unique tempdir leaf (robust against /var↔/private/var and
// logical-vs-physical pwd differences — the random component is identical
// either way). Unix-gated like the capture test (clean raw-pipe EOF).
#[cfg(unix)]
#[test]
fn spawn_program_in_lands_the_child_in_the_requested_cwd() {
    use std::io::Read;

    let dir = tempfile::tempdir().expect("tempdir");
    let leaf = dir
        .path()
        .file_name()
        .expect("tempdir has a final component")
        .to_string_lossy()
        .into_owned();

    let session = PtySession::spawn_program_in(
        "sh",
        ["-c", "pwd"],
        SurfaceSize::new(24, 80),
        Some(dir.path()),
    )
    .expect("spawn pwd child in the requested cwd");

    let mut reader = session.take_reader().expect("take reader");
    let mut out = Vec::new();
    reader.read_to_end(&mut out).expect("read child cwd to EOF");
    let text = String::from_utf8_lossy(&out);
    assert!(
        text.contains(&leaf),
        "child's pwd must be the requested cwd (leaf {leaf:?}), got: {text:?}"
    );

    let _ = session.wait().expect("reap child");
}

//! A1 regression tests for the native-PTY [`SessionSurface`] backend.
//!
//! Two guarantees:
//!  - **resize-under-load** does not leak/hang/panic (Spike #5) — runs on both
//!    OSes; the master lock must serialize a resize storm against itself.
//!  - **output is captured** off a real PTY — Unix-gated here (the `forkpty` raw
//!    pipe EOFs cleanly); the Windows/ConPTY capture path needs the A2 DSR
//!    auto-answer and is proven by that task's regression test.

use std::sync::Arc;
use std::thread;

use spt_term::{PtySession, SessionSurface, SurfaceSize};

/// A long-lived child, OS-appropriate, that stays alive while we hammer it.
fn long_lived() -> PtySession {
    #[cfg(unix)]
    let s = PtySession::spawn_program("sleep", ["30"], SurfaceSize::new(24, 80));
    #[cfg(windows)]
    let s = PtySession::spawn_program("ping", ["-n", "30", "127.0.0.1"], SurfaceSize::new(24, 80));
    s.expect("spawn long-lived child under a pty")
}

// [unit->REQ-TERM-1]
#[test]
fn resize_under_load_does_not_hang_or_panic() {
    let session = Arc::new(long_lived());
    assert!(session.process_id().is_some(), "a real PTY child has a pid");

    // Three concurrent resizers contend the master lock: the storm must
    // serialize cleanly (no panic, no error, no deadlock).
    let mut handles = Vec::new();
    for t in 0..3u16 {
        let s = Arc::clone(&session);
        handles.push(thread::spawn(move || {
            for i in 0..150u16 {
                let rows = 10 + ((t * 7 + i) % 40);
                let cols = 40 + ((t * 13 + i) % 80);
                s.resize(SurfaceSize::new(rows, cols))
                    .expect("resize under contention must not fail");
            }
        }));
    }
    for h in handles {
        h.join().expect("resizer thread must not panic");
    }

    // Surface still usable after the storm; tear the child down cleanly.
    session
        .resize(SurfaceSize::new(24, 80))
        .expect("post-storm resize");
    session.kill().expect("kill child");
    let _ = session.wait().expect("reap child after kill");
}

// [unit->REQ-TERM-1]
#[cfg(unix)]
#[test]
fn captures_child_output_off_the_pty() {
    use std::io::Read;

    let session = PtySession::spawn_program(
        "sh",
        ["-c", "printf 'hello-surface\\n'"],
        SurfaceSize::new(24, 80),
    )
    .expect("spawn echo child");

    let mut reader = session.take_reader().expect("take the master reader");
    // Second take is rejected — output has exactly one consumer.
    assert!(session.take_reader().is_err(), "reader is once-only");

    let mut out = Vec::new();
    reader
        .read_to_end(&mut out)
        .expect("read child output to EOF");
    let text = String::from_utf8_lossy(&out);
    assert!(
        text.contains("hello-surface"),
        "expected child stdout in the captured stream, got: {text:?}"
    );

    let _ = session.wait().expect("reap child");
}

+// [unit->REQ-HOST-RUN-2] `spawn_program_in` lands the child in the requested

+// working directory: a child that prints its cwd reports the dir we passed.

+// Asserted on the unique tempdir leaf (robust against /var↔/private/var and

+// logical-vs-physical pwd differences — the random component is identical

+// either way). Unix-gated like the capture test (clean raw-pipe EOF).

+    let dir = tempfile::tempdir().expect("tempdir");

+        .expect("tempdir has a final component")

+    let session = PtySession::spawn_program_in(

+    .expect("spawn pwd child in the requested cwd");

+    let mut reader = session.take_reader().expect("take reader");

+    reader.read_to_end(&mut out).expect("read child cwd to EOF");

+    let text = String::from_utf8_lossy(&out);

+        "child's pwd must be the requested cwd (leaf {leaf:?}), got: {text:?}"

+    let _ = session.wait().expect("reap child");


Now the int test through the broker. Read broker.rs test harness (spawn + output read pattern):


1	//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output
2	//! and injected input over the versioned local IPC, on both OSes.
3	//!
4	//! The child is an echo program (`cat` on Unix, `findstr "."` on Windows — the
5	//! same cross-OS shape spt-term's injection tests use): bytes the brain injects
6	//! round-trip through the real PTY and come back as `output` events. We then
7	//! kill it and observe the `exit` event. This exercises the whole B1 contract —
8	//! spawn under a broker PTY, stream output, accept input, report exit — through
9	//! the actual named-pipe / UDS transport.
10	//!
11	//! The brain is driven **single-threaded**: send a command, then read frames
12	//! until the expected reply/event arrives (accumulating any interleaved output).
13	//! interprocess `Stream::split()` shares the underlying fd, so a split client
14	//! would only signal EOF once *both* halves drop; keeping one `Stream` means
15	//! dropping it closes the fd, the broker's command loop hits EOF, and it tears
16	//! the session down deterministically — which we then assert.
17	// [int->REQ-DAEMON-2]
18
19	use std::sync::atomic::{AtomicU32, Ordering};
20	use std::sync::Arc;
21	use std::thread;
22	use std::time::Duration;
23
24	use interprocess::local_socket::Stream;
25	use spt_daemon::codec::{read_frame, write_frame};
26	use spt_daemon::frame::{Envelope, Role};
27	use spt_daemon::msg::{
28	    decode_bytes, encode_bytes, ExitEvent, InputReq, KillReq, OutputEvent, SpawnReq, Spawned,
29	    KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED,
30	};
31	use spt_daemon::transport::{send_hello, LocalSocketTransport};
32	use spt_daemon::Broker;
33
34	static SEQ: AtomicU32 = AtomicU32::new(0);
35	fn unique_name() -> String {
36	    let n = SEQ.fetch_add(1, Ordering::Relaxed);
37	    format!("spt-daemon-b1-{}-{}.sock", std::process::id(), n)
38	}
39
40	/// The echo child: reads stdin, writes each line to stdout.
41	fn echo_spawn_req() -> SpawnReq {
42	    #[cfg(unix)]
43	    let (program, args) = ("cat".to_string(), Vec::<String>::new());
44	    #[cfg(windows)]
45	    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
46	    SpawnReq {
47	        program,
48	        args,
49	        rows: 24,
50	        cols: 80,
51	        endpoint: String::new(),
52	        cwd: None,
53	    }
54	}
55
56	fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {
57	    write_frame(conn, &Envelope::new(kind, payload)).expect("send frame");
58	}
59
60	fn main_test() {
61	    let name = unique_name();
62	    let broker = Broker::bind(&name).expect("bind broker");
63
64	    // The broker accepts one brain and serves it on a handler thread.
65	    let server_broker = Arc::clone(&broker);
66	    let server = thread::spawn(move || {
67	        let handle = server_broker.serve_once().expect("accept brain");
68	        handle.join().expect("handler thread");
69	    });
70
71	    // Brain side: connect (retry briefly in case accept lags the bind).
72	    let mut conn = None;
73	    for _ in 0..200 {
74	        match LocalSocketTransport::connect(&name) {
75	            Ok(c) => {
76	                conn = Some(c);
77	                break;
78	            }
79	            Err(_) => thread::sleep(Duration::from_millis(10)),
80	        }
81	    }
82	    let mut conn = conn.expect("connect to broker");
83	    // (No per-read timeout: Windows named pipes reject I/O timeouts. The broker
84	    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads
85	    // below return; a genuine stall is caught by the test runner's own timeout.)
86
87	    send_hello(&mut conn, Role::Brain).expect("send hello");
88
89	    // Spawn the echo child under a broker PTY; read until the `spawned` reply,
90	    // accumulating any output that interleaves.
91	    send(
92	        &mut conn,
93	        KIND_SPAWN,
94	        serde_json::to_value(echo_spawn_req()).unwrap(),
95	    );
96	    let mut out = Vec::<u8>::new();
97	    let sid = loop {
98	        let f = read_frame(&mut conn).expect("frame before spawned");
99	        match f.kind.as_str() {
100	            KIND_SPAWNED => {
101	                break serde_json::from_value::<Spawned>(f.payload)
102	                    .unwrap()
103	                    .session_id;
104	            }
105	            KIND_OUTPUT => accumulate_output(&f, &mut out),
106	            _ => {}
107	        }
108	    };
109
110	    // Inject a line; it must round-trip through the real PTY back as output.
111	    send(
112	        &mut conn,
113	        KIND_INPUT,
114	        serde_json::to_value(InputReq {
115	            session_id: sid,
116	            data_b64: encode_bytes(b"INJECT-ALPHA\r"),
117	            op_id: None,
118	        })
119	        .unwrap(),
120	    );

//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output
//! and injected input over the versioned local IPC, on both OSes.
//!
//! The child is an echo program (`cat` on Unix, `findstr "."` on Windows — the
//! same cross-OS shape spt-term's injection tests use): bytes the brain injects
//! round-trip through the real PTY and come back as `output` events. We then
//! kill it and observe the `exit` event. This exercises the whole B1 contract —
//! spawn under a broker PTY, stream output, accept input, report exit — through
//! the actual named-pipe / UDS transport.
//!
//! The brain is driven **single-threaded**: send a command, then read frames
//! until the expected reply/event arrives (accumulating any interleaved output).
//! interprocess `Stream::split()` shares the underlying fd, so a split client
//! would only signal EOF once *both* halves drop; keeping one `Stream` means
//! dropping it closes the fd, the broker's command loop hits EOF, and it tears
//! the session down deterministically — which we then assert.
// [int->REQ-DAEMON-2]

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use interprocess::local_socket::Stream;
use spt_daemon::codec::{read_frame, write_frame};
use spt_daemon::frame::{Envelope, Role};
use spt_daemon::msg::{
    decode_bytes, encode_bytes, ExitEvent, InputReq, KillReq, OutputEvent, SpawnReq, Spawned,
    KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED,
};
use spt_daemon::transport::{send_hello, LocalSocketTransport};
use spt_daemon::Broker;

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique_name() -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-b1-{}-{}.sock", std::process::id(), n)
}

/// The echo child: reads stdin, writes each line to stdout.
fn echo_spawn_req() -> SpawnReq {
    #[cfg(unix)]
    let (program, args) = ("cat".to_string(), Vec::<String>::new());
    #[cfg(windows)]
    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: String::new(),
        cwd: None,
    }
}

fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {
    write_frame(conn, &Envelope::new(kind, payload)).expect("send frame");
}

fn main_test() {
    let name = unique_name();
    let broker = Broker::bind(&name).expect("bind broker");

    // The broker accepts one brain and serves it on a handler thread.
    let server_broker = Arc::clone(&broker);
    let server = thread::spawn(move || {
        let handle = server_broker.serve_once().expect("accept brain");
        handle.join().expect("handler thread");
    });

    // Brain side: connect (retry briefly in case accept lags the bind).
    let mut conn = None;
    for _ in 0..200 {
        match LocalSocketTransport::connect(&name) {
            Ok(c) => {
                conn = Some(c);
                break;
            }
            Err(_) => thread::sleep(Duration::from_millis(10)),
        }
    }
    let mut conn = conn.expect("connect to broker");
    // (No per-read timeout: Windows named pipes reject I/O timeouts. The broker
    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads
    // below return; a genuine stall is caught by the test runner's own timeout.)

    send_hello(&mut conn, Role::Brain).expect("send hello");

    // Spawn the echo child under a broker PTY; read until the `spawned` reply,
    // accumulating any output that interleaves.
    send(
        &mut conn,
        KIND_SPAWN,
        serde_json::to_value(echo_spawn_req()).unwrap(),
    );
    let mut out = Vec::<u8>::new();
    let sid = loop {
        let f = read_frame(&mut conn).expect("frame before spawned");
        match f.kind.as_str() {
            KIND_SPAWNED => {
                break serde_json::from_value::<Spawned>(f.payload)
                    .unwrap()
                    .session_id;
            }
            KIND_OUTPUT => accumulate_output(&f, &mut out),
            _ => {}
        }
    };

    // Inject a line; it must round-trip through the real PTY back as output.
    send(
        &mut conn,
        KIND_INPUT,
        serde_json::to_value(InputReq {
            session_id: sid,
            data_b64: encode_bytes(b"INJECT-ALPHA\r"),
            op_id: None,
        })
        .unwrap(),
    );


Add the `[int->REQ-HOST-RUN-2]` broker-level test to attach.rs (has Brain + net_broker + read_output_until + tempfile). Append after the local loopback test:


428	        .spawn_session(echo_spawn_req())
429	        .expect("spawn echo child");
430
431	    // The operator acquires the broker's loopback conn; its `remote_id_hex` is
432	    // this node's OWN broker-minted id — the origin the peer row carries and the
433	    // serve gates same-node against.
434	    let mut operator = connect_retry(&name);
435	    let dialed = operator.net_dial_loopback().expect("operator loopback dial");
436	    let own_origin = dialed.remote_id_hex.clone();
437	    let (_viewport, mut target) = attach_drive_detach(
438	        &mut operator,
439	        target,
440	        dialed.conn_id,
441	        sid,
442	        "ling",
443	        &own_origin,
444	        b"LOCAL_DRIVE_OK",
445	    );
446	    assert_eq!(
447	        broker.session_count(),
448	        1,
449	        "the session outlives the local viewport"
450	    );
451	    target.kill_session().expect("cleanup kill");
452	}
453
454	// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain
455	// crash mid-attach: the successor re-serves the SAME broker-held stream from
456	// seq 0 (worst-case stale cursor) — the replayed Request/input/output all
457	// dedup through the journals, input B sent INTO the dead window arrives, and
458	// B's viewport holds every marker exactly once.
459	// [unit->REQ-INST-8]
460	#[test]
461	fn attach_survives_target_brain_restart_exactly_once() {
462	    init_home();
463	    let dir = tempfile::tempdir().expect("tempdir");
464	    let (name_a, name_b) = (unique_name(), unique_name());
465	    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
466	    let _broker_b = net_broker(&name_b, &dir.path().join("b"));
467

        .spawn_session(echo_spawn_req())
        .expect("spawn echo child");

    // The operator acquires the broker's loopback conn; its `remote_id_hex` is
    // this node's OWN broker-minted id — the origin the peer row carries and the
    // serve gates same-node against.
    let mut operator = connect_retry(&name);
    let dialed = operator.net_dial_loopback().expect("operator loopback dial");
    let own_origin = dialed.remote_id_hex.clone();
    let (_viewport, mut target) = attach_drive_detach(
        &mut operator,
        target,
        dialed.conn_id,
        sid,
        "ling",
        &own_origin,
        b"LOCAL_DRIVE_OK",
    );
    assert_eq!(
        broker.session_count(),
        1,
        "the session outlives the local viewport"
    );
    target.kill_session().expect("cleanup kill");
}

// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain
// crash mid-attach: the successor re-serves the SAME broker-held stream from
// seq 0 (worst-case stale cursor) — the replayed Request/input/output all
// dedup through the journals, input B sent INTO the dead window arrives, and
// B's viewport holds every marker exactly once.
// [unit->REQ-INST-8]
#[test]
fn attach_survives_target_brain_restart_exactly_once() {
    init_home();
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, &dir.path().join("b"));


466:    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {
467-        loop {
468-            if contains(buf, needle) {
469-                return Ok(self.next_seq.saturating_sub(1));
470-            }
471-            match self.read_event()? {
472-                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),
473-                BrokerEvent::Exit { .. } => {
474-                    return Err(io::Error::new(
--
703:    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
704-        self.session_id = Some(session_id);
705-        self.next_seq = from_seq;
706-        // A resume-mode brain (non-empty map) routes this session's output through
707-        // the per-session map via `read_event`'s first-sight `or_insert` seed
708-        // (D4-2b); a legacy brain (empty map) uses the `next_seq` set above.
709-        self.subscribe(session_id, from_seq)
710-    }
711-


Add the int test (keep child alive after printing cwd to avoid exit race):


// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain
// crash mid-attach: the successor re-serves the SAME broker-held stream from
// seq 0 (worst-case stale cursor) — the replayed Request/input/output all
// dedup through the journals, input B sent INTO the dead window arrives, and
// B's viewport holds every marker exactly once.
// [unit->REQ-INST-8]
#[test]
fn attach_survives_target_brain_restart_exactly_once() {

// [int->REQ-HOST-RUN-2] the broker spawns the PTY child in the requested
// PROJECT cwd: a `SpawnReq.cwd` rides the wire through the broker into the
// portable-pty CommandBuilder, and a child that prints its cwd reports the dir
// we asked for — end to end (wire field → broker spawn → PTY cwd). Asserted on
// the unique tempdir leaf (robust against path canonicalization). The child
// prints its cwd then blocks (cat/findstr) so the read never races an exit.
#[test]
fn broker_spawns_the_pty_child_in_the_requested_cwd() {
    init_home();
    let dir = tempfile::tempdir().expect("tempdir");
    let proj = dir.path().join("the-project-root");
    std::fs::create_dir_all(&proj).expect("project dir");
    let leaf = "the-project-root";

    let name = unique_name();
    let _broker = net_broker(&name, &dir.path().join("broker"));
    let mut brain = connect_retry(&name);

    // A child that prints its cwd, then stays alive on stdin (no exit race).
    #[cfg(unix)]
    let (program, args) = (
        "sh".to_string(),
        vec!["-c".to_string(), "pwd; cat".to_string()],
    );
    #[cfg(windows)]
    let (program, args) = (
        "cmd".to_string(),
        vec!["/c".to_string(), "cd & findstr \".\"".to_string()],
    );

    let sid = brain
        .spawn_session(SpawnReq {
            program,
            args,
            rows: 24,
            cols: 80,
            endpoint: String::new(),
            cwd: Some(proj.to_string_lossy().into_owned()),
        })
        .expect("spawn cwd-printing child in the project cwd");

    brain.attach(sid, 0).expect("attach to read output");
    let mut out = Vec::new();
    brain
        .read_output_until(leaf.as_bytes(), &mut out)
        .expect("child prints its cwd");
    assert!(
        contains(&out, leaf.as_bytes()),
        "the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}",
        String::from_utf8_lossy(&out)
    );
    brain.kill_session().expect("cleanup kill");
}

// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain
// crash mid-attach: the successor re-serves the SAME broker-held stream from
// seq 0 (worst-case stale cursor) — the replayed Request/input/output all
// dedup through the journals, input B sent INTO the dead window arrives, and
// B's viewport holds every marker exactly once.
// [unit->REQ-INST-8]
#[test]
fn attach_survives_target_brain_restart_exactly_once() {


// [int->REQ-HOST-RUN-2] the broker spawns the PTY child in the requested
// PROJECT cwd: a `SpawnReq.cwd` rides the wire through the broker into the
// portable-pty CommandBuilder, and a child that prints its cwd reports the dir
// we asked for — end to end (wire field → broker spawn → PTY cwd). Asserted on
// the unique tempdir leaf (robust against path canonicalization). The child
// prints its cwd then blocks (cat/findstr) so the read never races an exit.
#[test]
fn broker_spawns_the_pty_child_in_the_requested_cwd() {
    init_home();
    let dir = tempfile::tempdir().expect("tempdir");
    let proj = dir.path().join("the-project-root");
    std::fs::create_dir_all(&proj).expect("project dir");
    let leaf = "the-project-root";

    let name = unique_name();
    let _broker = net_broker(&name, &dir.path().join("broker"));
    let mut brain = connect_retry(&name);

    // A child that prints its cwd, then stays alive on stdin (no exit race).
    #[cfg(unix)]
    let (program, args) = (
        "sh".to_string(),
        vec!["-c".to_string(), "pwd; cat".to_string()],
    );
    #[cfg(windows)]
    let (program, args) = (
        "cmd".to_string(),
        vec!["/c".to_string(), "cd & findstr \".\"".to_string()],
    );

    let sid = brain
        .spawn_session(SpawnReq {
            program,
            args,
            rows: 24,
            cols: 80,
            endpoint: String::new(),
            cwd: Some(proj.to_string_lossy().into_owned()),
        })
        .expect("spawn cwd-printing child in the project cwd");

    brain.attach(sid, 0).expect("attach to read output");
    let mut out = Vec::new();
    brain
        .read_output_until(leaf.as_bytes(), &mut out)
        .expect("child prints its cwd");
    assert!(
        contains(&out, leaf.as_bytes()),
        "the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}",
        String::from_utf8_lossy(&out)
    );
    brain.kill_session().expect("cleanup kill");
}

// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain
// crash mid-attach: the successor re-serves the SAME broker-held stream from
// seq 0 (worst-case stale cursor) — the replayed Request/input/output all
// dedup through the journals, input B sent INTO the dead window arrives, and
// B's viewport holds every marker exactly once.
// [unit->REQ-INST-8]
#[test]
fn attach_survives_target_brain_restart_exactly_once() {

+// [int->REQ-HOST-RUN-2] the broker spawns the PTY child in the requested

+// PROJECT cwd: a `SpawnReq.cwd` rides the wire through the broker into the

+// portable-pty CommandBuilder, and a child that prints its cwd reports the dir

+// we asked for — end to end (wire field → broker spawn → PTY cwd). Asserted on

+// the unique tempdir leaf (robust against path canonicalization). The child

+// prints its cwd then blocks (cat/findstr) so the read never races an exit.

+    let proj = dir.path().join("the-project-root");

+    std::fs::create_dir_all(&proj).expect("project dir");

+    let _broker = net_broker(&name, &dir.path().join("broker"));

+    let mut brain = connect_retry(&name);

+    // A child that prints its cwd, then stays alive on stdin (no exit race).

+        vec!["-c".to_string(), "pwd; cat".to_string()],

+        vec!["/c".to_string(), "cd & findstr \".\"".to_string()],

+            cwd: Some(proj.to_string_lossy().into_owned()),

+        .expect("spawn cwd-printing child in the project cwd");

+    brain.attach(sid, 0).expect("attach to read output");

+        .read_output_until(leaf.as_bytes(), &mut out)

+        .expect("child prints its cwd");

+        contains(&out, leaf.as_bytes()),

+        "the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}",

+    brain.kill_session().expect("cleanup kill");


`contains` is a local closure, not module-level. Use the existing top-level `count` helper instead:


    brain
        .read_output_until(leaf.as_bytes(), &mut out)
        .expect("child prints its cwd");
    assert!(
        contains(&out, leaf.as_bytes()),
        "the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}",
        String::from_utf8_lossy(&out)
    );
    brain.kill_session().expect("cleanup kill");

    brain
        .read_output_until(leaf.as_bytes(), &mut out)
        .expect("child prints its cwd");
    assert!(
        count(&out, leaf.as_bytes()) >= 1,
        "the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}",
        String::from_utf8_lossy(&out)
    );
    brain.kill_session().expect("cleanup kill");


    brain
        .read_output_until(leaf.as_bytes(), &mut out)
        .expect("child prints its cwd");
    assert!(
        contains(&out, leaf.as_bytes()),
        "the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}",
        String::from_utf8_lossy(&out)
    );
    brain.kill_session().expect("cleanup kill");

    brain
        .read_output_until(leaf.as_bytes(), &mut out)
        .expect("child prints its cwd");
    assert!(
        count(&out, leaf.as_bytes()) >= 1,
        "the child's cwd must be the requested project dir (leaf {leaf:?}), got: {:?}",
        String::from_utf8_lossy(&out)
    );
    brain.kill_session().expect("cleanup kill");

-        contains(&out, leaf.as_bytes()),

+        count(&out, leaf.as_bytes()) >= 1,


Now activate REQ-HOST-RUN-2 stages in the registry (evidence now lands):


id = "REQ-HOST-RUN-2"
title = "Project-scoped working directory for spt-hosted bringup: `spt endpoint run` lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc <id>` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14)."
required_stages = []

id = "REQ-HOST-RUN-2"
title = "Project-scoped working directory for spt-hosted bringup: `spt endpoint run` lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc <id>` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14)."
required_stages = ["impl", "unit", "int"]  # activated M12-W1.5: impl = additive SpawnReq.cwd + PtySession::spawn_program_in (portable-pty CommandBuilder cwd) + broker dispatch_spawn honors it + harnesshost/cli thread project cwd (std::env::current_dir); unit = spawn_program_in_lands_the_child_in_the_requested_cwd (spt-term); int = broker_spawns_the_pty_child_in_the_requested_cwd (wire field → broker spawn → PTY cwd, end to end)


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

id = "REQ-HOST-RUN-2"
title = "Project-scoped working directory for spt-hosted bringup: `spt endpoint run` lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc <id>` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14)."
required_stages = []

id = "REQ-HOST-RUN-2"
title = "Project-scoped working directory for spt-hosted bringup: `spt endpoint run` lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc <id>` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14)."
required_stages = ["impl", "unit", "int"]  # activated M12-W1.5: impl = additive SpawnReq.cwd + PtySession::spawn_program_in (portable-pty CommandBuilder cwd) + broker dispatch_spawn honors it + harnesshost/cli thread project cwd (std::env::current_dir); unit = spawn_program_in_lands_the_child_in_the_requested_cwd (spt-term); int = broker_spawns_the_pty_child_in_the_requested_cwd (wire field → broker spawn → PTY cwd, end to end)

+required_stages = ["impl", "unit", "int"]  # activated M12-W1.5: impl = additive SpawnReq.cwd + PtySession::spawn_program_in (portable-pty CommandBuilder cwd) + broker dispatch_spawn honors it + harnesshost/cli thread project cwd (std::env::current_dir); unit = spawn_program_in_lands_the_child_in_the_requested_cwd (spt-term); int = broker_spawns_the_pty_child_in_the_requested_cwd (wire field → broker spawn → PTY cwd, end to end)

# ───────────────────────────── Install / migration / infra ──────────────────


All wiring + tests + activation done. Compile-check and run the new tests:


Bash

echo "=== spt-term cwd unit ==="; cargo test -p spt-term --test surface spawn_program_in 2>&1 | tail -12; echo "=== spt-daemon REQ-HOST-RUN-2 int + loopback ==="; cargo test -p spt-daemon --test attach cwd 2>&1 | tail -12

Run new REQ-HOST-RUN-2 tests


Command running in background with ID: b0td82au1. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b0td82au1.output. You will be notified when it completes. To check interim output, use Read on that file path.


Awaiting new-test results.


1	=== spt-term cwd unit ===
2	   Compiling spt-proto v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-proto)
3	   Compiling spt-store v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
4	   Compiling spt-msg v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
5	   Compiling spt-term v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
6	    Finished `test` profile [unoptimized + debuginfo] target(s) in 4.39s
7	     Running tests\surface.rs (target\debug\deps\surface-8b020198d4347f57.exe)
8
9	running 0 tests
10
11	test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
12
13	=== spt-daemon REQ-HOST-RUN-2 int + loopback ===
14

=== spt-term cwd unit ===
   Compiling spt-proto v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-proto)
   Compiling spt-store v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling spt-msg v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-term v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 4.39s
     Running tests\surface.rs (target\debug\deps\surface-8b020198d4347f57.exe)

running 0 tests

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

=== spt-daemon REQ-HOST-RUN-2 int + loopback ===


<task-notification>
<task-id>b0td82au1</task-id>
<tool-use-id>toolu_019F3gpu23XeZesKfzci12Rv</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b0td82au1.output</output-file>
<status>completed</status>
<summary>Background command "Run new REQ-HOST-RUN-2 tests" completed (exit code 0)</summary>
</task-notification>


spt-term unit is `#[cfg(unix)]` (clean-EOF gate, same as existing capture test) — filtered on this Windows box; runs on Linux CI. The cross-platform **int** test is the Windows-runnable proof. Awaiting it:


13	=== spt-daemon REQ-HOST-RUN-2 int + loopback ===
14	   Compiling spt-term v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
15	   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
16	    Finished `test` profile [unoptimized + debuginfo] target(s) in 7.37s
17	     Running tests\attach.rs (target\debug\deps\attach-b46a93ad5a7fc526.exe)
18
19	running 1 test
20	test broker_spawns_the_pty_child_in_the_requested_cwd ... ok
21
22	test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.08s
23
24

=== spt-daemon REQ-HOST-RUN-2 int + loopback ===
   Compiling spt-term v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 7.37s
     Running tests\attach.rs (target\debug\deps\attach-b46a93ad5a7fc526.exe)

running 1 test
test broker_spawns_the_pty_child_in_the_requested_cwd ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.08s


Prompt:
<task-notification>
<task-id>b0td82au1</task-id>
<tool-use-id>toolu_019F3gpu23XeZesKfzci12Rv</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b0td82au1.output</output-file>
<status>completed</status>
<summary>Background command "Run new REQ-HOST-RUN-2 tests" completed (exit code 0)</summary>
</task-notification>


Int test passes on Windows (`cmd /c cd` reported the project leaf). Now the full gate sweep — suite (real exit), clippy, traceable:


echo "=== SUITE ==="; cargo test -p spt-daemon -p spt -p spt-term 2>&1 | tee "C:\Users\decid\AppData\Local\Temp\spt-hr2-suite.txt" >/dev/null; echo "SUITE_EXIT=${PIPESTATUS[0]}"; grep -oE "[0-9]+ passed" "C:\Users\decid\AppData\Local\Temp\spt-hr2-suite.txt" | awk '{s+=$1} END{print s" passed total"}'; grep -E "test result: FAILED|[1-9][0-9]* failed" "C:\Users\decid\AppData\Local\Temp\spt-hr2-suite.txt" || echo "NO FAILURES"; echo "=== CLIPPY ==="; cargo clippy -p spt-daemon -p spt -p spt-term --all-targets -- -D warnings 2>&1 | tail -4; echo "CLIPPY_EXIT=${PIPESTATUS[0]}"; echo "=== TRACEABLE ==="; traceable-reqs check 2>&1 | grep -iE "HOST-RUN-2|RC-1|[1-9][0-9]* (finding|missing)|FAIL" ; echo "TRACEABLE_EXIT=$?"

Full gate sweep: suite + clippy + traceable


Command running in background with ID: bplrigkm0. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bplrigkm0.output. You will be notified when it completes. To check interim output, use Read on that file path.


Awaiting gate sweep. (Note: the chained `$?` after the traceable grep is grep's exit, not traceable's — I'll confirm traceable's real exit separately.)


1	=== SUITE ===
2

=== SUITE ===


Suite still running. Awaiting completion notification.


<task-notification>
<task-id>bplrigkm0</task-id>
<tool-use-id>toolu_012oiZciLpmcVcVhWSAzG6So</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bplrigkm0.output</output-file>
<status>completed</status>
<summary>Background command "Full gate sweep: suite + clippy + traceable" completed (exit code 0)</summary>
</task-notification>


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


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


1	=== SUITE ===
2	SUITE_EXIT=0
3	527 passed total
4	NO FAILURES
5	=== CLIPPY ===
6	    Checking spt-term v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
7	    Checking spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
8	    Checking spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
9	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.91s
10	CLIPPY_EXIT=0
11	=== TRACEABLE ===
12	       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
13	       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
14	       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
15	       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
16	       Stale registry entries degrade to fallback, never hard-fail (4.3)
17	  [OK] REQ-HOST-RUN-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
18	       cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)
19	       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
20	  [OK] REQ-RC-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
21	       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
22	TRACEABLE_EXIT=0
23

=== SUITE ===
SUITE_EXIT=0
527 passed total
NO FAILURES
=== CLIPPY ===
    Checking spt-term v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Checking spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.91s
CLIPPY_EXIT=0
=== TRACEABLE ===
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
  [OK] REQ-HOST-RUN-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
  [OK] REQ-RC-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
TRACEABLE_EXIT=0


All green: **527 passed, 0 failures, SUITE_EXIT=0; CLIPPY_EXIT=0; REQ-HOST-RUN-2 [OK] impl+unit+int.** Confirm traceable's real exit (prior `$?` was grep's):


traceable-reqs check >$null 2>&1; echo "TRACEABLE_REAL_EXIT=$?"

traceable real exit code


/usr/bin/bash: line 4: $null: ambiguous redirect
TRACEABLE_REAL_EXIT=1


Bash `$null` typo — that exit was the redirect error, not traceable. Retry:


traceable-reqs check >/dev/null 2>&1; echo "TRACEABLE_REAL_EXIT=$?"

traceable real exit code


traceable real EXIT=0. **REQ-HOST-RUN-2 complete** — all gate criteria met. Ping doyle for the gate verdict + update memory:


$OWL ring doyle todlando <<'EOF'
REQ-HOST-RUN-2 DONE — gate verdict requested (the last must-land you named before the M12 gate).

DELIVERED (additive, N-1 safe):
- `SpawnReq.cwd: Option<String>` (serde default + skip_if_none) — N-1 brain/broker omits it, inherits broker cwd.
- spt-term `PtySession::spawn_program_in(..., cwd)` → portable-pty `CommandBuilder::cwd`; `spawn_program` delegates with None (back-compat).
- broker `dispatch_spawn` honors `req.cwd.as_deref()`.
- harnesshost `launch_harness_brokered_in(..., cwd: Option<&str>)` threads it into SpawnReq.
- cli `cmd_endpoint_run` derives the PROJECT cwd from `std::env::current_dir()` (where `cc <id>` was invoked) — failure falls back to None, never aborts.
- shells unchanged (cwd: None — explicitly out of scope).

EVIDENCE:
- unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like the existing capture test — clean raw-pipe EOF; runs on Linux CI).
- int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM; PASSES on this Windows box — wire field → broker spawn → PTY cwd end to end; child prints cwd, asserts the unique project-dir leaf).
- full spt+spt-daemon+spt-term suite: 527 passed / 0 failed. clippy -D EXIT=0. traceable EXIT=0 — REQ-HOST-RUN-2 [OK] impl+unit+int.
- UNCOMMITTED (operator-gated, m12-w1-bringup-rc).

GATE PASS? After this, full-fat M12 still wants W2 picker → W3 manifest → W4 subnet QR → W5 whoami — holding for your call + operator on whether to continue the waves or checkpoint here.
EOF

Ring doyle for REQ-HOST-RUN-2 gate


Command running in background with ID: bg83go946. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bg83go946.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bg83go946</task-id>
<tool-use-id>toolu_019b5Ht72u1jspPQ22R9gx6z</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bg83go946.output</output-file>
<status>completed</status>
<summary>Background command "Ring doyle for REQ-HOST-RUN-2 gate" completed (exit code 0)</summary>
</task-notification>


Update memory while doyle reviews:


Edit

**NEXT (after W1.5 gate verdict):** **REQ-HOST-RUN-2** (project cwd via additive `SpawnReq.cwd`) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

**REQ-HOST-RUN-2 DONE (todlando 2026-06-14) — gate pinged doyle, verdict pending.** Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.

**NEXT (after gate verdict + operator call):** full-fat M12 remaining waves — W2 picker → W3 manifest → W4 subnet QR → W5 whoami. W1.x bringup+rc foundation now complete (spt-hosted bringup + cross-node + LOCAL rc + project cwd). Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\m12-w1-progress.md has been updated successfully. (file state is current in your context — no need to Read it back)

**REQ-HOST-RUN-2 DONE (todlando 2026-06-14) — gate pinged doyle, verdict pending.** Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.

**NEXT (after gate verdict + operator call):** full-fat M12 remaining waves — W2 picker → W3 manifest → W4 subnet QR → W5 whoami. W1.x bringup+rc foundation now complete (spt-hosted bringup + cross-node + LOCAL rc + project cwd). Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

---
name: m12-w1-progress
description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
metadata:
  node_type: memory
  type: project
  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
---

M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).

**Built + green:**
- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).

**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.

**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.

**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).

**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).

**W1.5 GATE PASS (doyle 2026-06-14, independently verified — read shared helper + both int tests + reproduced trace).** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

**NEXT (after W1.5 gate verdict):** **REQ-HOST-RUN-2** (project cwd via additive `SpawnReq.cwd`) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

-**NEXT (after W1.5 gate verdict):** **REQ-HOST-RUN-2** (project cwd via additive `SpawnReq.cwd`) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

+**REQ-HOST-RUN-2 DONE (todlando 2026-06-14) — gate pinged doyle, verdict pending.** Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.

+**NEXT (after gate verdict + operator call):** full-fat M12 remaining waves — W2 picker → W3 manifest → W4 subnet QR → W5 whoami. W1.x bringup+rc foundation now complete (spt-hosted bringup + cross-node + LOCAL rc + project cwd). Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).


Prompt:
<task-notification>
<task-id>bg83go946</task-id>
<tool-use-id>toolu_019b5Ht72u1jspPQ22R9gx6z</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bg83go946.output</output-file>
<status>completed</status>
<summary>Background command "Ring doyle for REQ-HOST-RUN-2 gate" completed (exit code 0)</summary>
</task-notification>


Both bodies of work done. doyle gate verdict pending (arrives via poll).

**Status — W1.5 + REQ-HOST-RUN-2 complete:**

| Wave | What | Gate |
|---|---|---|
| W1.5 | B1 broker loopback-conn; LOCAL `spt rc` rides byte-identical pump | ✅ PASS (doyle) |
| REQ-HOST-RUN-2 | project cwd via additive `SpawnReq.cwd` → portable-pty | pinged doyle, pending |

**Evidence (combined):** full spt+spt-daemon+spt-term suite **527 passed / 0 failed**; clippy -D EXIT=0; traceable EXIT=0 — REQ-RC-1 [OK] + REQ-HOST-RUN-2 [OK], both impl+unit+int.

**Uncommitted** on `m12-w1-bringup-rc` (operator-gated).

W1.x bringup+rc foundation now complete: spt-hosted bringup + cross-node attach + local attach + project cwd. Full-fat M12 still wants **W2 picker → W3 manifest → W4 subnet QR → W5 whoami**.

Awaiting doyle's REQ-HOST-RUN-2 verdict. After that — continue to W2, or checkpoint here for operator? Your call.


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="36d869c4" type="msg" from="doyle">REQ-HOST-RUN-2 GATE: PASS. ✅ Verified: traceable EXIT=0 (impl+unit+int [OK]); SpawnReq.cwd is genuinely additive (serde default + skip_if_none → N-1 omit inherits); int test broker_spawns_the_pty_child_in_the_requested_cwd is real end-to-end (child prints cwd, asserts the project-dir leaf — wire→broker→Command</EVENT-PART>
<EVENT-PART seq="2/3" id="36d869c4">Builder.cwd). shells unchanged (None). Trusting 527/0 + clippy -D.

That closes the confirmed-missing gating foundation: spt-hosted harness bringup + local &amp; cross-node PTY attach + project cwd. The hard part of M12 is DONE + gated.

CONTINUE-vs-CHECKPOINT (W2 picker / W3 manifest / W4 subnet QR / W5 whoami): routing to the ope</EVENT-PART>
<EVENT-PART seq="3/3" id="36d869c4">rator now — it's a pacing/scope call. HOLD building until I relay their decision. If they say continue, my suggested order is W3 first (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code), then W2/W4/W5. Sit tight; verdict shortly. — doyle</EVENT-PART></event>
</task-notification>


<task-notification>
<task-id>b5qrsnbto</task-id>
<tool-use-id>toolu_015xfKTcdfF4Kh2ZKSMDJfMG</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b5qrsnbto.output</output-file>
<status>completed</status>
<summary>Background command "Ack doyle HOST-RUN-2 gate + holding" completed (exit code 0)</summary>
</task-notification>


**REQ-HOST-RUN-2 GATE PASS** (doyle). The confirmed-missing gating foundation is DONE + gated: spt-hosted bringup + local & cross-node PTY attach + project cwd. doyle routes the continue-vs-checkpoint call to operator (you) and says HOLD building until you decide.

Ack doyle + update memory, then surface the decision:


Bash

$OWL send doyle todlando <<'EOF'
REQ-HOST-RUN-2 GATE PASS received. Holding all building per your call. Surfacing CONTINUE-vs-CHECKPOINT to operator now; your suggested order noted (W3 first — file-backed strings + adapter:profile fallback, most directly unblocks spt-claude-code → then W2/W4/W5). Will relay their decision before touching code.
EOF

Ack doyle HOST-RUN-2 gate + holding


Command running in background with ID: b5qrsnbto. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b5qrsnbto.output. You will be notified when it completes. To check interim output, use Read on that file path.


Prompt:
<task-notification>
<task-id>b5qrsnbto</task-id>
<tool-use-id>toolu_015xfKTcdfF4Kh2ZKSMDJfMG</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b5qrsnbto.output</output-file>
<status>completed</status>
<summary>Background command "Ack doyle HOST-RUN-2 gate + holding" completed (exit code 0)</summary>
</task-notification>


Edit

**REQ-HOST-RUN-2 DONE (todlando 2026-06-14) — gate pinged doyle, verdict pending.**

**REQ-HOST-RUN-2 GATE PASS (doyle 2026-06-14) — verified end-to-end.** doyle: "the confirmed-missing gating foundation is DONE + gated; the hard part of M12 is done." CONTINUE-vs-CHECKPOINT (W2-W5) routed to OPERATOR; HOLD building until operator relays. doyle suggested order if continue: **W3 first** (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code) → then W2/W4/W5.