/// Detach the calling process from its console (`FreeConsole`) — the broker's
/// self-detach for the auto-start ladder's window-allocating rungs. The WMI /
/// schtasks rungs launch the broker via `cmd /c … start "" /b spt daemon run`;
/// @15fdf58 set DETACHED_PROCESS on the cmd WRAPPER, but `start /b` then launched
/// the inner broker (a console-subsystem exe) with no console to inherit, so it
/// ALLOCATED ITS OWN conhost = a visible window (REQ-HAZARD-WMI-DAEMON-WINDOW;
/// violating REQ-INSTALL-10's no-persistent-window invariant). Calling FreeConsole
/// at broker startup — gated on the hidden `--detached` flag the ladder appends —
/// drops that console so the conhost exits. Launch-agnostic + idempotent: a no-op
/// on the breakaway/injob rungs (already DETACHED_PROCESS ⇒ no console ⇒ FreeConsole
/// returns 0/err, ignored), and a foreground human `spt daemon run` omits the flag
/// so its terminal view is preserved.
// [impl->REQ-HAZARD-WMI-DAEMON-WINDOW]
#[cfg(windows)]
pub fn detach_console() {
    extern "system" {
        fn FreeConsole() -> i32;
    }
    // SAFETY: a parameterless kernel32 call; failure (no console attached on the
    // already-windowless rungs) is expected and ignored.
    unsafe {
        let _ = FreeConsole();
    }
}

/// Build the command line a job-neutral creator (WMI / Task Scheduler) runs to
/// launch the daemon.

+/// Detach the calling process from its console (`FreeConsole`) — the broker's

+/// self-detach for the auto-start ladder's window-allocating rungs. The WMI /

+/// schtasks rungs launch the broker via `cmd /c … start "" /b spt daemon run`;

+/// @15fdf58 set DETACHED_PROCESS on the cmd WRAPPER, but `start /b` then launched

+/// the inner broker (a console-subsystem exe) with no console to inherit, so it

+/// ALLOCATED ITS OWN conhost = a visible window (REQ-HAZARD-WMI-DAEMON-WINDOW;

+/// violating REQ-INSTALL-10's no-persistent-window invariant). Calling FreeConsole

+/// at broker startup — gated on the hidden `--detached` flag the ladder appends —

+/// drops that console so the conhost exits. Launch-agnostic + idempotent: a no-op

+/// on the breakaway/injob rungs (already DETACHED_PROCESS ⇒ no console ⇒ FreeConsole

+/// returns 0/err, ignored), and a foreground human `spt daemon run` omits the flag

+    // SAFETY: a parameterless kernel32 call; failure (no console attached on the

+    // already-windowless rungs) is expected and ignored.


Now the clap `Run` variant + dispatch + `cmd_daemon_run`:


Edit

    /// Run the per-machine daemon in the FOREGROUND — this process IS the
    /// daemon, blocking until signalled (the service unit's ExecStart, or
    /// manual debugging). Never detaches; for a background daemon use `start`.
    Run,

    /// Run the per-machine daemon in the FOREGROUND — this process IS the
    /// daemon, blocking until signalled (the service unit's ExecStart, or
    /// manual debugging). Never detaches; for a background daemon use `start`.
    Run {
        /// Hidden (REQ-HAZARD-WMI-DAEMON-WINDOW): set by the auto-start LADDER on
        /// every rung so the broker FreeConsole()s its own console at startup (the
        /// WMI/schtasks `start /b` rungs otherwise allocate a conhost window).
        /// Absent on a human-typed `spt daemon run` → foreground console preserved.
        #[arg(long, hide = true)]
        detached: bool,
    },


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

    /// Run the per-machine daemon in the FOREGROUND — this process IS the
    /// daemon, blocking until signalled (the service unit's ExecStart, or
    /// manual debugging). Never detaches; for a background daemon use `start`.
    Run,

    /// Run the per-machine daemon in the FOREGROUND — this process IS the
    /// daemon, blocking until signalled (the service unit's ExecStart, or
    /// manual debugging). Never detaches; for a background daemon use `start`.
    Run {
        /// Hidden (REQ-HAZARD-WMI-DAEMON-WINDOW): set by the auto-start LADDER on
        /// every rung so the broker FreeConsole()s its own console at startup (the
        /// WMI/schtasks `start /b` rungs otherwise allocate a conhost window).
        /// Absent on a human-typed `spt daemon run` → foreground console preserved.
        #[arg(long, hide = true)]
        detached: bool,
    },

+        /// Hidden (REQ-HAZARD-WMI-DAEMON-WINDOW): set by the auto-start LADDER on

+        /// every rung so the broker FreeConsole()s its own console at startup (the

+        /// WMI/schtasks `start /b` rungs otherwise allocate a conhost window).

+        /// Absent on a human-typed `spt daemon run` → foreground console preserved.

/// Ensure the daemon is up in the background (idempotent, service-aware):

/// a registered OS service is driven via its manager, else a detached


DaemonCmd::Run { detached } => cmd_daemon_run(detached),


// The daemon noun (M8 decisions 5, 25b): bare = the node status view.

Cmd::Daemon { action } => match action.unwrap_or(DaemonCmd::Status) {

-            DaemonCmd::Run => cmd_daemon_run(),

+            DaemonCmd::Run { detached } => cmd_daemon_run(detached),


fn cmd_daemon_run() -> i32 {
    #[cfg(windows)]
    if spt_daemon::service::run_refused_when_vanishing(

fn cmd_daemon_run(detached: bool) -> i32 {
    // Broker self-detach (REQ-HAZARD-WMI-DAEMON-WINDOW): when the auto-start ladder
    // launched us with `--detached`, drop our console FIRST so the conhost the
    // WMI/schtasks `start /b` rung allocated exits — before any output or bind. A
    // human-typed foreground `spt daemon run` omits the flag and keeps its console.
    #[cfg(windows)]
    if detached {
        spt_daemon::daemon::detach_console();
    }
    #[cfg(not(windows))]
    let _ = detached; // unix detaches via setsid in spawn_detached; flag unused.
    #[cfg(windows)]
    if spt_daemon::service::run_refused_when_vanishing(


fn cmd_daemon_run() -> i32 {
    #[cfg(windows)]
    if spt_daemon::service::run_refused_when_vanishing(

fn cmd_daemon_run(detached: bool) -> i32 {
    // Broker self-detach (REQ-HAZARD-WMI-DAEMON-WINDOW): when the auto-start ladder
    // launched us with `--detached`, drop our console FIRST so the conhost the
    // WMI/schtasks `start /b` rung allocated exits — before any output or bind. A
    // human-typed foreground `spt daemon run` omits the flag and keeps its console.
    #[cfg(windows)]
    if detached {
        spt_daemon::daemon::detach_console();
    }
    #[cfg(not(windows))]
    let _ = detached; // unix detaches via setsid in spawn_detached; flag unused.
    #[cfg(windows)]
    if spt_daemon::service::run_refused_when_vanishing(

+fn cmd_daemon_run(detached: bool) -> i32 {

+    // Broker self-detach (REQ-HAZARD-WMI-DAEMON-WINDOW): when the auto-start ladder

+    // launched us with `--detached`, drop our console FIRST so the conhost the

+    // WMI/schtasks `start /b` rung allocated exits — before any output or bind. A

+    // human-typed foreground `spt daemon run` omits the flag and keeps its console.

+        spt_daemon::daemon::detach_console();

+    let _ = detached; // unix detaches via setsid in spawn_detached; flag unused.


Now fix the broken parse test (was `DaemonCmd::Run` unit variant):


8012	        );
8013	    }
8014
8015	    // [unit->REQ-CLI-2] [unit->REQ-DAEMON-7] the daemon noun parses: bare (=
8016	    // status view), run/start/stop/status. `start` is now its OWN first-class
8017	    // verb (background, service-aware) — NO longer an alias of foreground
8018	    // `run` (REQ-DAEMON-7): the two must parse to distinct variants.
8019	    #[test]
8020	    fn daemon_subcommands_parse() {
8021	        assert!(matches!(
8022	            parse(&["spt", "daemon"]).unwrap().cmd.unwrap(),
8023	            Cmd::Daemon { action: None }
8024	        ));
8025	        assert!(matches!(
8026	            parse(&["spt", "daemon", "run"]).unwrap().cmd.unwrap(),
8027	            Cmd::Daemon {
8028	                action: Some(DaemonCmd::Run)
8029	            }

        );
    }

    // [unit->REQ-CLI-2] [unit->REQ-DAEMON-7] the daemon noun parses: bare (=
    // status view), run/start/stop/status. `start` is now its OWN first-class
    // verb (background, service-aware) — NO longer an alias of foreground
    // `run` (REQ-DAEMON-7): the two must parse to distinct variants.
    #[test]
    fn daemon_subcommands_parse() {
        assert!(matches!(
            parse(&["spt", "daemon"]).unwrap().cmd.unwrap(),
            Cmd::Daemon { action: None }
        ));
        assert!(matches!(
            parse(&["spt", "daemon", "run"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Run)
            }


8025	        assert!(matches!(
8026	            parse(&["spt", "daemon", "run"]).unwrap().cmd.unwrap(),
8027	            Cmd::Daemon {
8028	                action: Some(DaemonCmd::Run)
8029	            }
8030	        ));
8031	        // `start` graduated from a `run` alias to its own verb.
8032	        assert!(matches!(
8033	            parse(&["spt", "daemon", "start"]).unwrap().cmd.unwrap(),
8034	            Cmd::Daemon {
8035	                action: Some(DaemonCmd::Start)
8036	            }
8037	        ));
8038	        assert!(matches!(
8039	            parse(&["spt", "daemon", "stop"]).unwrap().cmd.unwrap(),
8040	            Cmd::Daemon {
8041	                action: Some(DaemonCmd::Stop)
8042	            }
8043	        ));
8044	        assert!(matches!(

        assert!(matches!(
            parse(&["spt", "daemon", "run"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Run)
            }
        ));
        // `start` graduated from a `run` alias to its own verb.
        assert!(matches!(
            parse(&["spt", "daemon", "start"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Start)
            }
        ));
        assert!(matches!(
            parse(&["spt", "daemon", "stop"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Stop)
            }
        ));
        assert!(matches!(


        assert!(matches!(
            parse(&["spt", "daemon", "run"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Run)
            }
        ));
        // `start` graduated from a `run` alias to its own verb.

        // A human-typed `daemon run` parses with `detached: false` → cmd_daemon_run
        // keeps its foreground console; the hidden `--detached` (the auto-start
        // ladder's signal) parses to `true` → the FreeConsole self-detach branch
        // (REQ-HAZARD-WMI-DAEMON-WINDOW; assert the flag routing, not the syscall).
        // [unit->REQ-HAZARD-WMI-DAEMON-WINDOW]
        assert!(matches!(
            parse(&["spt", "daemon", "run"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Run { detached: false })
            }
        ));
        assert!(matches!(
            parse(&["spt", "daemon", "run", "--detached"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Run { detached: true })
            }
        ));
        // `start` graduated from a `run` alias to its own verb.


        assert!(matches!(
            parse(&["spt", "daemon", "run"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Run)
            }
        ));
        // `start` graduated from a `run` alias to its own verb.

        // A human-typed `daemon run` parses with `detached: false` → cmd_daemon_run
        // keeps its foreground console; the hidden `--detached` (the auto-start
        // ladder's signal) parses to `true` → the FreeConsole self-detach branch
        // (REQ-HAZARD-WMI-DAEMON-WINDOW; assert the flag routing, not the syscall).
        // [unit->REQ-HAZARD-WMI-DAEMON-WINDOW]
        assert!(matches!(
            parse(&["spt", "daemon", "run"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Run { detached: false })
            }
        ));
        assert!(matches!(
            parse(&["spt", "daemon", "run", "--detached"]).unwrap().cmd.unwrap(),
            Cmd::Daemon {
                action: Some(DaemonCmd::Run { detached: true })
            }
        ));
        // `start` graduated from a `run` alias to its own verb.

+        // A human-typed `daemon run` parses with `detached: false` → cmd_daemon_run

+        // keeps its foreground console; the hidden `--detached` (the auto-start

+        // ladder's signal) parses to `true` → the FreeConsole self-detach branch

+        // (REQ-HAZARD-WMI-DAEMON-WINDOW; assert the flag routing, not the syscall).

+        // [unit->REQ-HAZARD-WMI-DAEMON-WINDOW]

-                action: Some(DaemonCmd::Run)

+                action: Some(DaemonCmd::Run { detached: false })

+            parse(&["spt", "daemon", "run", "--detached"]).unwrap().cmd.unwrap(),

+                action: Some(DaemonCmd::Run { detached: true })


Check for any other `DaemonCmd::Run` references that need updating:


crates\spt\src\cli.rs:1174:            DaemonCmd::Run { detached } => cmd_daemon_run(detached),
crates\spt\src\cli.rs:8033:                action: Some(DaemonCmd::Run { detached: false })
crates\spt\src\cli.rs:8039:                action: Some(DaemonCmd::Run { detached: true })


All references consistent. Now reactivate the REQ registry (`[impl,unit,int]` + extended note):


required_stages = ["impl", "unit"]  # activated v0.12.2: impl = wmi_create_script builds the Win32_ProcessStartup no-window spec (CreateFlags=8 DETACHED + ShowWindow=0) passed as ProcessStartupInformation to Win32_Process.Create. unit = wmi_create_script_carries_the_no_window_startup_spec (spec present + passed + DETACHED, never the invalid CREATE_NO_WINDOW). WMI launch VALIDITY (a bad spec → ReturnValue!=0 → no daemon) is independently guarded by job_escape_e2e (the REQ-HAZARD-VIEWER-CLOSE-DETACH int red-flagged the invalid CREATE_NO_WINDOW during the fix).

required_stages = ["impl", "unit", "int"]  # activated v0.12.2: impl = wmi_create_script builds the Win32_ProcessStartup no-window spec (CreateFlags=8 DETACHED + ShowWindow=0) passed as ProcessStartupInformation to Win32_Process.Create. unit = wmi_create_script_carries_the_no_window_startup_spec (spec present + passed + DETACHED, never the invalid CREATE_NO_WINDOW). WMI launch VALIDITY (a bad spec → ReturnValue!=0 → no daemon) is independently guarded by job_escape_e2e (the REQ-HAZARD-VIEWER-CLOSE-DETACH int red-flagged the invalid CREATE_NO_WINDOW during the fix). REOPENED v0.13.0 (todlando 2026-06-19, doyle /diagnose): the v0.12.2 fix was INSUFFICIENT — it set DETACHED_PROCESS on the cmd WRAPPER, but the WMI/schtasks rungs then run `start "" /b spt daemon run`, launching the inner BROKER (a console-subsystem exe) with no console to inherit → the broker ALLOCATES ITS OWN conhost = a window (deterministic 3/3: stop → any spt cmd auto-starts via ensure_running → WMI rung → window). FIX = broker self-detach: the auto-start ladder appends a hidden `--detached` flag (spawn_detached args, all rungs) and cmd_daemon_run calls FreeConsole() at startup when set (launch-agnostic; no-op on the already-windowless breakaway/injob rungs; a human foreground `spt daemon run` omits the flag → keeps its console). int ADDED now: a real WMI-cold-start int (job_escape_e2e wmi_daemon_run_has_no_conhost_window) asserts the WMI-launched broker pid has NO conhost.exe child (RED pre-fix: found 1 conhost; GREEN post-fix), with full daemon-tree reap hygiene. impl = spawn_detached --detached arg + daemon::detach_console (FreeConsole) + cmd_daemon_run gate; unit = daemon_subcommands_parse extended (run ⇒ detached:false, run --detached ⇒ true — flag routing); int = no-conhost-child of the WMI broker.


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)

title = "`spt daemon start` launches the daemon with NO visible console window. REGRESSION (v0.12.1 L1.5): the WMI job-neutral launch (spawn_daemon_via_wmi) set CREATE_NO_WINDOW on the launching powershell but NOT on the Win32_Process.Create call — Win32_Process.Create does not inherit it, so the spawned cmd.exe env-forwarding wrapper popped a console window on every cold-start (violating REQ-INSTALL-10's v0.7.4 no-persistent-window invariant; the old detached_no_inherit path set DETACHED_PROCESS|CREATE_NO_WINDOW). FIX: pass a Win32_ProcessStartup with CreateFlags=DETACHED_PROCESS (0x8 — no console so no window; CREATE_NO_WINDOW 0x08000000 is NOT a valid Win32_ProcessStartup flag → ReturnValue 21 invalid-param, which is why the naive port fails) + ShowWindow=SW_HIDE(0) belt, via the ProcessStartupInformation argument. (v0.12.2)"

-required_stages = ["impl", "unit"]  # activated v0.12.2: impl = wmi_create_script builds the Win32_ProcessStartup no-window spec (CreateFlags=8 DETACHED + ShowWindow=0) passed as ProcessStartupInformation to Win32_Process.Create. unit = wmi_create_script_carries_the_no_window_startup_spec (spec present + passed + DETACHED, never the invalid CREATE_NO_WINDOW). WMI launch VALIDITY (a bad spec → ReturnValue!=0 → no daemon) is independently guarded by job_escape_e2e (the REQ-HAZARD-VIEWER-CLOSE-DETACH int red-flagged the invalid CREATE_NO_WINDOW during the fix).

+required_stages = ["impl", "unit", "int"]  # activated v0.12.2: impl = wmi_create_script builds the Win32_ProcessStartup no-window spec (CreateFlags=8 DETACHED + ShowWindow=0) passed as ProcessStartupInformation to Win32_Process.Create. unit = wmi_create_script_carries_the_no_window_startup_spec (spec present + passed + DETACHED, never the invalid CREATE_NO_WINDOW). WMI launch VALIDITY (a bad spec → ReturnValue!=0 → no daemon) is independently guarded by job_escape_e2e (the REQ-HAZARD-VIEWER-CLOSE-DETACH int red-flagged the invalid CREATE_NO_WINDOW during the fix). REOPENED v0.13.0 (todlando 2026-06-19, doyle /diagnose): the v0.12.2 fix was INSUFFICIENT — it set DETACHED_PROCESS on the cmd WRAPPER, but the WMI/schtasks rungs then run `start "" /b spt daemon run`, launching the inner BROKER (a console-subsystem exe) with no console to inherit → the broker ALLOCATES ITS OWN conhost = a window (deterministic 3/3: stop → any spt cmd auto-starts via ensure_running → WMI rung → window). FIX = broker self-detach: the auto-start ladder appends a hidden `--detached` flag (spawn_detached args, all rungs) and cmd_daemon_run calls FreeConsole() at startup when set (launch-agnostic; no-op on the already-windowless breakaway/injob rungs; a human foreground `spt daemon run` omits the flag → keeps its console). int ADDED now: a real WMI-cold-start int (job_escape_e2e wmi_daemon_run_has_no_conhost_window) asserts the WMI-launched broker pid has NO conhost.exe child (RED pre-fix: found 1 conhost; GREEN post-fix), with full daemon-tree reap hygiene. impl = spawn_detached --detached arg + daemon::detach_console (FreeConsole) + cmd_daemon_run gate; unit = daemon_subcommands_parse extended (run ⇒ detached:false, run --detached ⇒ true — flag routing); int = no-conhost-child of the WMI broker.

# ───────────────────────────── v0.12.2 CLI polish (command-output markdown render) ──────────


Fix + registry applied, all refs consistent. Now gates. Start with workspace clippy (CI form — compiles all targets) + the parse unit:


PowerShell

cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 12

Workspace clippy CI form


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


Clippy clean. Now the parse unit + traceability gate:


cargo test -p spt --bin spt daemon_subcommands_parse 2>&1 | Select-Object -Last 6; "----"; traceable-reqs check > $null 2>&1; "TRACEABLE_EXIT=$LASTEXITCODE"

Run daemon parse unit + traceability gate


running 1 test
test cli::tests::daemon_subcommands_parse ... ok

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

----
TRACEABLE_EXIT=0


Unit green, traceable EXIT=0. Now the load-bearing GREEN — run the int against the **fixed** bin (confirms the conhost window is gone):


cargo test -p spt --test job_escape_e2e wmi_daemon_run_has_no_conhost_window -- --nocapture 2>&1 | Select-Object -Last 20

Run WMI no-conhost int against fixed bin (expect GREEN)


Error: Exit code 101
   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 5.86s
     Running tests\job_escape_e2e.rs (target\debug\deps\job_escape_e2e-3547b2895875fc1b.exe)

running 1 test

thread 'win::wmi_daemon_run_has_no_conhost_window' (41364) panicked at crates\spt\tests\job_escape_e2e.rs:259:17:
PRECONDITION: the WMI-launched daemon never bound (brain.ready / daemon.pid missing) in the isolated SPT_HOME (rung marker="WMI"). If SPT_HOME did not reach the WMI child, the daemon bound the wrong universe.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test win::wmi_daemon_run_has_no_conhost_window ... FAILED

failures:

failures:
    win::wmi_daemon_run_has_no_conhost_window

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

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


Int PRECONDITION-failed: the daemon never bound post-fix (`daemon.pid`/`brain.ready` missing) — the fix broke bringup. Root cause: after `FreeConsole()` the inherited console std handles are invalid, and Rust's `println!`/`eprintln!` **panic** writing to a non-null invalid handle → broker dies before binding. (The DETACHED_PROCESS rungs are safe because their handles are NULL = Rust's safe no-op path.) Fix: NULL the std handles after FreeConsole so stdio becomes a safe no-op.


Edit

// [impl->REQ-HAZARD-WMI-DAEMON-WINDOW]
#[cfg(windows)]
pub fn detach_console() {
    extern "system" {
        fn FreeConsole() -> i32;
    }
    // SAFETY: a parameterless kernel32 call; failure (no console attached on the
    // already-windowless rungs) is expected and ignored.
    unsafe {
        let _ = FreeConsole();
    }
}

// [impl->REQ-HAZARD-WMI-DAEMON-WINDOW]
#[cfg(windows)]
pub fn detach_console() {
    extern "system" {
        fn FreeConsole() -> i32;
        fn SetStdHandle(which: u32, handle: isize) -> i32;
    }
    // STD_*_HANDLE = (DWORD)-10/-11/-12.
    const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6;
    const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5;
    const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;
    // SAFETY: parameterless / scalar kernel32 calls.
    unsafe {
        let _ = FreeConsole();
        // CRITICAL: after FreeConsole the inherited console std handles are INVALID
        // (the WMI/schtasks `start /b` rung gave the broker a real conhost, now
        // freed). A non-null INVALID handle makes Rust's `println!`/`eprintln!`
        // PANIC on write — which would kill the broker before it binds. NULL the
        // three std handles so stdio degrades to a safe no-op, exactly the
        // no-console state the DETACHED_PROCESS rungs (breakaway/injob) already run
        // in (Rust treats a null std handle as a discard-success write).
        let _ = SetStdHandle(STD_INPUT_HANDLE, 0);
        let _ = SetStdHandle(STD_OUTPUT_HANDLE, 0);
        let _ = SetStdHandle(STD_ERROR_HANDLE, 0);
    }
}


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

// [impl->REQ-HAZARD-WMI-DAEMON-WINDOW]
#[cfg(windows)]
pub fn detach_console() {
    extern "system" {
        fn FreeConsole() -> i32;
    }
    // SAFETY: a parameterless kernel32 call; failure (no console attached on the
    // already-windowless rungs) is expected and ignored.
    unsafe {
        let _ = FreeConsole();
    }
}

// [impl->REQ-HAZARD-WMI-DAEMON-WINDOW]
#[cfg(windows)]
pub fn detach_console() {
    extern "system" {
        fn FreeConsole() -> i32;
        fn SetStdHandle(which: u32, handle: isize) -> i32;
    }
    // STD_*_HANDLE = (DWORD)-10/-11/-12.
    const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6;
    const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5;
    const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;
    // SAFETY: parameterless / scalar kernel32 calls.
    unsafe {
        let _ = FreeConsole();
        // CRITICAL: after FreeConsole the inherited console std handles are INVALID
        // (the WMI/schtasks `start /b` rung gave the broker a real conhost, now
        // freed). A non-null INVALID handle makes Rust's `println!`/`eprintln!`
        // PANIC on write — which would kill the broker before it binds. NULL the
        // three std handles so stdio degrades to a safe no-op, exactly the
        // no-console state the DETACHED_PROCESS rungs (breakaway/injob) already run
        // in (Rust treats a null std handle as a discard-success write).
        let _ = SetStdHandle(STD_INPUT_HANDLE, 0);
        let _ = SetStdHandle(STD_OUTPUT_HANDLE, 0);
        let _ = SetStdHandle(STD_ERROR_HANDLE, 0);
    }
}

+        fn SetStdHandle(which: u32, handle: isize) -> i32;

-    // SAFETY: a parameterless kernel32 call; failure (no console attached on the

-    // already-windowless rungs) is expected and ignored.

+    const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6;

+    const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5;

+    const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;

+    // SAFETY: parameterless / scalar kernel32 calls.

+        // CRITICAL: after FreeConsole the inherited console std handles are INVALID

+        // (the WMI/schtasks `start /b` rung gave the broker a real conhost, now

+        // freed). A non-null INVALID handle makes Rust's `println!`/`eprintln!`

+        // PANIC on write — which would kill the broker before it binds. NULL the

+        // three std handles so stdio degrades to a safe no-op, exactly the

+        // no-console state the DETACHED_PROCESS rungs (breakaway/injob) already run

+        // in (Rust treats a null std handle as a discard-success write).

+        let _ = SetStdHandle(STD_INPUT_HANDLE, 0);

+        let _ = SetStdHandle(STD_OUTPUT_HANDLE, 0);

+        let _ = SetStdHandle(STD_ERROR_HANDLE, 0);


Re-run the int against the corrected fix:


PowerShell

cargo test -p spt --test job_escape_e2e wmi_daemon_run_has_no_conhost_window -- --nocapture 2>&1 | Select-Object -Last 16

Re-run WMI no-conhost int (expect GREEN)


   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 8.68s
     Running tests\job_escape_e2e.rs (target\debug\deps\job_escape_e2e-3547b2895875fc1b.exe)

running 1 test
=== DIAGNOSTIC: rung_marker="WMI" broker_pid=211384 brain_pid=191392 no_conhost=true final_conhost_count=0 tree_reaped=true ===
test win::wmi_daemon_run_has_no_conhost_window ... ok

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


GREEN: `no_conhost=true`, `final_conhost_count=0`, daemon bound (broker+brain pids), `tree_reaped=true`. RED→GREEN confirmed — the broker self-detaches its window AND still binds. My fix touches the shared daemon bringup, so run the **full** `job_escape_e2e` (the original survival test exercises the same WMI path) + real-bin build + final traceable:


cargo test -p spt --test job_escape_e2e 2>&1 | Select-Object -Last 10; "---BUILD---"; cargo build -p spt --bin spt 2>&1 | Select-Object -Last 3; "---TRACE---"; traceable-reqs check > $null 2>&1; "TRACEABLE_EXIT=$LASTEXITCODE"

Full job_escape_e2e + real bin build + traceability


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running tests\job_escape_e2e.rs (target\debug\deps\job_escape_e2e-3547b2895875fc1b.exe)

running 2 tests
test win::wmi_daemon_run_has_no_conhost_window ... ok
test win::cold_start_daemon_escapes_kill_on_close_job_via_wmi ... ok

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

---BUILD---
   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.72s
---TRACE---
TRACEABLE_EXIT=0


All gates green: both `job_escape_e2e` tests pass (new no-conhost + original survival = no regression) · clippy `-D warnings`=0 · parse unit green · real bin builds · traceable EXIT=0. Commit the fix:


Stage the daemon-window fix


warning: in the working copy of 'crates/spt/tests/job_escape_e2e.rs', LF will be replaced by CRLF the next time Git touches it
M  crates/spt-daemon/src/daemon.rs
M  crates/spt/src/cli.rs
M  crates/spt/tests/job_escape_e2e.rs
M  traceable-reqs.toml


git commit -m @'
fix(daemon): broker FreeConsole self-detach — kill the WMI/schtasks daemon-window respawn (REQ-HAZARD-WMI-DAEMON-WINDOW reopened)

The v0.12.2 fix (@15fdf58) was INSUFFICIENT. It set DETACHED_PROCESS on the cmd
WRAPPER, but the WMI/schtasks rungs run `cmd /c set… & start "" /b spt daemon run`,
and `start /b` then launches the inner BROKER (a console-subsystem exe) with no
console to inherit — so the broker ALLOCATES ITS OWN conhost = a visible window.
Deterministic (doyle /diagnose, 3/3): `daemon stop` then any spt cmd auto-starts
via ensure_running -> WMI rung -> window. (The respawn itself is by-design; only
the WINDOW is the bug.)

FIX (broker self-detach, launch-agnostic): the auto-start ladder appends a hidden
`--detached` flag to the daemon-run args (spawn_detached, every rung); cmd_daemon_run
calls daemon::detach_console() at startup when set — FreeConsole() then NULLs the
three std handles so stdio degrades to the safe no-op the DETACHED_PROCESS rungs
already run in (a non-null INVALID console handle after FreeConsole makes Rust's
println!/eprintln! PANIC — that would kill the broker before it binds). No-op on
the breakaway/injob rungs (already windowless); a human foreground `spt daemon run`
omits the flag and keeps its console.

Repro-first: the WMI no-conhost int was landed RED before the fix (found 1 conhost
child of the WMI broker pid) and is GREEN after (0 conhost, daemon still binds), with
full daemon-tree reap hygiene. The original job-escape int still passes (no regression).

Gates: workspace clippy -D warnings = 0; daemon parse unit + both job_escape_e2e ints
green; traceable-reqs check EXIT=0 (REQ-HAZARD-WMI-DAEMON-WINDOW reactivated impl/unit/int);
real spt bin rebuilds. doyle gates next.

Co-authored by: todlando
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVVByRoZnz85acTkVJ1uAP
'@

Commit the WMI daemon-window fix


[v0.13.0-delivery-control 082f5ed] fix(daemon): broker FreeConsole self-detach — kill the WMI/schtasks daemon-window respawn (REQ-HAZARD-WMI-DAEMON-WINDOW reopened)
 4 files changed, 295 insertions(+), 6 deletions(-)


Committed @082f5ed. Notify doyle for gate — including the load-bearing extra finding (FreeConsole alone broke bringup; std-handle NULL is required):


Bash

$OWL send doyle <<'EOF'
WMI daemon-window fix READY FOR GATE @082f5ed (REQ-HAZARD-WMI-DAEMON-WINDOW reopened → impl/unit/int, registry-first in this commit).

Repro-first held your way: engineer landed the no-conhost int RED first (rung=WMI, found 1 conhost child of the broker pid, durable 5s, tree_reaped=true), THEN I fixed → GREEN (no_conhost=true, final_conhost_count=0, daemon still binds, tree_reaped=true). Original cold_start job-escape int still green (no regression).

FIX = broker self-detach, exactly your design: auto-start ladder appends hidden --detached (spawn_detached args, every rung); cmd_daemon_run → daemon::detach_console() FreeConsole() at startup when set. No-op on breakaway/injob (already windowless); foreground `spt daemon run` omits the flag (console preserved).

ONE load-bearing addition you'll want to see: FreeConsole() ALONE broke bringup — my first GREEN run PRECONDITION-failed (daemon never bound). Root cause: after FreeConsole the inherited console std handles are INVALID (non-null), and Rust println!/eprintln! PANIC writing to an invalid handle → broker dies before daemon.pid. Fix: detach_console() also SetStdHandle(NULL) on stdin/out/err so stdio degrades to the safe no-op the DETACHED_PROCESS rungs already run in. Caught it because the int's GREEN check failed first — repro-first earned its keep twice.

Gates I ran: clippy --workspace -D warnings=0; daemon_subcommands_parse unit (run⇒detached:false, run --detached⇒true — flag routing); both job_escape_e2e ints green; traceable EXIT=0; real spt bin rebuilds. NOT touched: the by-design respawn (ensure_running). Your deterministic gate loop (stop→marked start→no conhost child) = exactly what the int forces. Your gate.
EOF

Send daemon-window gate-request to doyle


➜ SENT:doyle


**WMI daemon-window fix shipped @082f5ed → doyle gating.**

REQ-HAZARD-WMI-DAEMON-WINDOW reopened (`@15fdf58` was insufficient — it detached the cmd wrapper, but `start /b` launched the inner broker which allocated its own conhost = the window).

- **Fix:** auto-start ladder appends hidden `--detached`; broker `FreeConsole()`s at startup + NULLs std handles. No-op on already-windowless rungs; foreground `daemon run` keeps its console.
- **Repro-first:** int landed RED (1 conhost child of WMI broker) → GREEN (0, daemon still binds). Original job-escape int still green.
- **Caught a second bug via the GREEN run:** FreeConsole alone broke bringup (Rust `eprintln!` panics on the post-FreeConsole invalid handle) — required the std-handle NULL. Repro-first earned its keep twice.
- **Gates:** clippy `-D warnings`=0 · parse unit · both `job_escape_e2e` ints · traceable EXIT=0 · real bin rebuilds.

Operator may fold into v0.13.0 (doyle confirms). Nothing blocking on my side — waiting on doyle's gate.


<task-notification>
<task-id>bv8r78fk1</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="4f6e5870" type="msg" from="doyle">doyle WINDOW-FIX GATE PASS — @082f5ed. Verified myself (not trust-green): nextest job_escape_e2e 2/2 PASS, **0 skipped** — wmi_daemon_run_has_no_conhost_window RAN non-vacuously (forces+verifies the WMI rung, RED-first, asserts 0 conhost children of the broker pid) = exactly my deterministic loop, hermetic; cold_sta</EVENT-PART>
<EVENT-PART seq="2/7" id="4f6e5870">rt job-escape no-regression; daemon_subcommands_parse flag-routing unit pass (run⇒false / run --detached⇒true); clippy --workspace -D warnings=0; traceable EXIT=0; docs-drift `xtask check: OK`. detach_console (FreeConsole + SetStdHandle NULL×3) is correct — the NULL-handles catch (Rust panics writing an invalid post-FreeConsole handle) </EVENT-PART>
<EVENT-PART seq="3/7" id="4f6e5870">is a real load-bearing find; matches the DETACHED rungs' stdio state. REQ-HAZARD-WMI-DAEMON-WINDOW [impl,unit,int] ✓. Repro-first earned its keep twice. Clean.

NEXT DISPATCH — bugs 3+4 (operator HITL, ONE root): cmd_endpoint_run (cli.rs:1318→1358) does launch_harness_brokered_in → run_attach with NO await-online. Both picker pat</EVENT-PART>
<EVENT-PART seq="4/7" id="4f6e5870">hs route here start=false (mod.rs dispatch RunMode::Attach→(false,false)): Start-now catches the endpoint mid-bringup → run_attach ATTEMPTS + fails the race; Resume catches it still fully OFFLINE → run_attach short-circuits &quot;offline — nothing to attach&quot; → never attempts. SAME root, two faces.
FIX: in cmd_endpoint_run, wh</EVENT-PART>
<EVENT-PART seq="5/7" id="4f6e5870">en the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach — poll spt_store::info status (alive+online / the bind) with a bounded harness-boot deadline (~20-30s), tight interval; on online→run_attach; on timeout→ENDPOINT_RUN err (don't attach a dead bringup). Fix</EVENT-PART>
<EVENT-PART seq="6/7" id="4f6e5870">es BOTH Start-now AND Resume. (Online endpoints already return Outcome::Attach, not Run — unaffected.)
REPRO-FIRST (dummy-harness fixture): bringup-then-attach must SUCCEED; pre-fix the immediate attach races/bails = RED. REQ: extend REQ-RUN-PICKER (W4) or mint a REQ-HAZARD-RC-ATTACH-ONLINE-RACE — your call, registry-first. clippy+trace</EVENT-PART>
<EVENT-PART seq="7/7" id="4f6e5870">able. Surface for gate.

bug 2 (special keys: arrows/Home/End dead) = a BIGGER fork (rc input layer) — holding its dispatch for the operator's scope ruling (quick VT-input vs proper agnostic key-event→xterm-VT translator; v0.13.0 vs fast-follow). Will send once ruled. Do window (done) + 3+4 first.</EVENT-PART></event>
</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.


doyle **WINDOW-FIX GATE PASS @082f5ed** — REQ-HAZARD-WMI-DAEMON-WINDOW `[impl,unit,int]` ✓, non-vacuous, docs-drift OK, NULL-handles catch confirmed load-bearing.

**Next dispatch — bugs 3+4 (ONE root):** `cmd_endpoint_run` does `launch_harness_brokered_in → run_attach` with NO await-online. Both picker attach paths (`RunMode::Attach`→start=false) race the bringup: Start-now attaches mid-boot (fails the race), Resume hits it fully offline (`run_attach` short-circuits "nothing to attach"). Same root, two faces — and this is exactly what my W4 item-2 (attach-by-default) needs to actually land. **Fix:** await endpoint online between `launch_harness_brokered_in` success and `run_attach` (attach/view only, not start); bounded ~20-30s deadline; timeout→err.

Grounding the fix sites:


1330	            // the long-lived PTY child off it); a backend without a pid prints `-`.
1331	            let pid_display = spawned
1332	                .pid
1333	                .map(|p| p.to_string())
1334	                .unwrap_or_else(|| "-".to_string());
1335	            eprintln!("ENDPOINT_RUN:{id} adapter={adapter} session={session_id} pid={pid_display}");
1336	        }
1337	        Err(e) => {
1338	            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
1339	            return 1;
1340	        }
1341	    }
1342
1343	    // Terminal action: start (return now) | view (read-only attach) | attach
1344	    // (default). Attach/view route through the SAME rc pump as `spt rc`.
1345	    if start {
1346	        // `ENDPOINT_RUN_STARTED:{id}` is the machine token (no Markdown markers →
1347	        // survives strip intact for a piped adapter); the `spt rc` hint restyles
1348	        // on a tty and strips clean otherwise. [impl->REQ-CLI-OUTPUT-MARKDOWN]
1349	        eprintln!(
1350	            "{}",
1351	            crate::helpfmt::render(
1352	                &format!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)"),
1353	                crate::helpfmt::stderr_color()
1354	            )
1355	        );
1356	        return 0;
1357	    }
1358	    match crate::rc::run_attach(id, rc_intent(view, false)) {
1359	        Ok(()) => 0,
1360	        Err(e) => {
1361	            eprintln!("RC_FAIL:{id}: {e}");
1362	            1
1363	        }
1364	    }
1365	}
1366
1367	/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move
1368	/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
1369	/// runs before anything mutates, so a refused fork leaves no half-fork.

            // the long-lived PTY child off it); a backend without a pid prints `-`.
            let pid_display = spawned
                .pid
                .map(|p| p.to_string())
                .unwrap_or_else(|| "-".to_string());
            eprintln!("ENDPOINT_RUN:{id} adapter={adapter} session={session_id} pid={pid_display}");
        }
        Err(e) => {
            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
            return 1;
        }
    }

    // Terminal action: start (return now) | view (read-only attach) | attach
    // (default). Attach/view route through the SAME rc pump as `spt rc`.
    if start {
        // `ENDPOINT_RUN_STARTED:{id}` is the machine token (no Markdown markers →
        // survives strip intact for a piped adapter); the `spt rc` hint restyles
        // on a tty and strips clean otherwise. [impl->REQ-CLI-OUTPUT-MARKDOWN]
        eprintln!(
            "{}",
            crate::helpfmt::render(
                &format!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)"),
                crate::helpfmt::stderr_color()
            )
        );
        return 0;
    }
    match crate::rc::run_attach(id, rc_intent(view, false)) {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("RC_FAIL:{id}: {e}");
            1
        }
    }
}

/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move
/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
/// runs before anything mutates, so a refused fork leaves no half-fork.


Grep

fn run_attach|offline|nothing to attach|STATUS_ONLINE|fn status|pub fn .*online|liveness


232:pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {
247:    // `status=offline` is not attachable — short-circuit with clean guidance
251:    // spt-hosted perch offline, so this catches the common case before any IPC. An
260:        == Some(spt_store::liveness::STATUS_OFFLINE)
262:        println!("Endpoint '{endpoint_id}' is offline — nothing to attach to.");
589:    // status=offline (the B2-reconcile / endpoint-stop state) SHORT-CIRCUITS with
593:    fn attach_offline_endpoint_short_circuits_clean() {
600:        spt_store::info::set_status(&perch, spt_store::liveness::STATUS_OFFLINE).unwrap();


Insertion point clear (after the `if start` early-return, before `run_attach`). Read `run_attach`'s status-read helper to reuse the same online signal:


232	pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {
233	    let view = intent == AttachIntent::Viewer;
234	    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to
235	    // an endpoint a DIFFERENT node already controls gets actionable guidance here
236	    // (the broker would also refuse on the wire — this is the friendly path). A
237	    // `--view` / `--take` bypasses it (watching coexists; taking displaces).
238	    if intent == AttachIntent::Control {
239	        if let Some(node) = current_driver(endpoint_id) {
240	            println!("'{endpoint_id}' is controlled by {node}.");
241	            println!("  spt rc {endpoint_id} --view   to watch read-only");
242	            println!("  spt rc {endpoint_id} --take   to take control (kicks {node})");
243	            return Ok(());
244	        }
245	    }
246	    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT
247	    // `status=offline` is not attachable — short-circuit with clean guidance
248	    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]
249	    // instead of the broker dance that would block on a dead session (the
250	    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead
251	    // spt-hosted perch offline, so this catches the common case before any IPC. An
252	    // ABSENT or status-less perch falls through to the broker path, which already
253	    // ends cleanly ("no live session for endpoint") — not a hang. (is_perch_alive
254	    // is NOT used here: it returns true for an absent info.json by interim parity.)
255	    let perch_path =
256	        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);
257	    if spt_store::info::read_info(&perch_path)
258	        .and_then(|i| i.status)
259	        .as_deref()
260	        == Some(spt_store::liveness::STATUS_OFFLINE)
261	    {
262	        println!("Endpoint '{endpoint_id}' is offline — nothing to attach to.");
263	        println!("  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it");
264	        return Ok(());
265	    }
266	    spt_daemon::daemon::ensure_running().map_err(|e| format!("daemon start: {e}"))?;
267	    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline
268	    // (a `Whole` conn can't time out — nonblocking is dead on Windows named
269	    // pipes), so the single-threaded pump interleaves stdin + broker events
270	    // without wedging. The generous io_timeout bounds the setup calls (dial,
271	    // sessions, attach-open ack); the pump itself passes its own short slice.
272	    let mut brain = Brain::cold_start_pump(
273	        &spt_daemon::endpoint::broker_socket_name(),
274	        now_ms(),
275	        Duration::from_secs(10),
276	    )
277	    .map_err(|e| format!("broker connect: {e}"))?;
278
279	    let session_id = resolve_session(&mut brain, endpoint_id)
280	        .map_err(|e| format!("list sessions: {e}"))?
281	        .ok_or_else(|| format!("no live session for endpoint '{endpoint_id}'"))?;
282
283	    let ops = OpMinter::new();
284	    // Transport: a broker-minted in-process LOOPBACK connection (M12 W1.5, B1).
285	    // A QUIC self-dial is refused by iroh ("Connecting to ourself is not
286	    // supported"), so local attach cannot ride a self-dialed conn; instead the
287	    // broker wires an in-process loopback conn whose streams surface as the SAME
288	    // net-stream events the cross-node QUIC path produces. From `request_attach`
289	    // down this is byte-identical to a cross-node attach — the one-pump invariant
290	    // lives at the transport leaf inside the broker, never here. The loopback
291	    // peer's origin is this node's own id, so the target's serve gates same-node.
292	    let dialed = brain
293	        .net_dial_loopback()
294	        .map_err(|e| format!("loopback dial: {e}"))?;
295	    let stream_id = request_attach(&mut brain, dialed.conn_id, session_id, 0, ops.next(), intent)
296	        .map_err(|e| format!("attach request: {e}"))?;
297	    // Subscribe our side of the stream so the target's `Output` records surface
298	    // as `NetStreamData` events (the open + Request do not auto-subscribe).
299	    brain
300	        .net_stream_subscribe(stream_id, 0)
301	        .map_err(|e| format!("stream subscribe: {e}"))?;

pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {
    let view = intent == AttachIntent::Viewer;
    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to
    // an endpoint a DIFFERENT node already controls gets actionable guidance here
    // (the broker would also refuse on the wire — this is the friendly path). A
    // `--view` / `--take` bypasses it (watching coexists; taking displaces).
    if intent == AttachIntent::Control {
        if let Some(node) = current_driver(endpoint_id) {
            println!("'{endpoint_id}' is controlled by {node}.");
            println!("  spt rc {endpoint_id} --view   to watch read-only");
            println!("  spt rc {endpoint_id} --take   to take control (kicks {node})");
            return Ok(());
        }
    }
    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT
    // `status=offline` is not attachable — short-circuit with clean guidance
    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]
    // instead of the broker dance that would block on a dead session (the
    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead
    // spt-hosted perch offline, so this catches the common case before any IPC. An
    // ABSENT or status-less perch falls through to the broker path, which already
    // ends cleanly ("no live session for endpoint") — not a hang. (is_perch_alive
    // is NOT used here: it returns true for an absent info.json by interim parity.)
    let perch_path =
        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);
    if spt_store::info::read_info(&perch_path)
        .and_then(|i| i.status)
        .as_deref()
        == Some(spt_store::liveness::STATUS_OFFLINE)
    {
        println!("Endpoint '{endpoint_id}' is offline — nothing to attach to.");
        println!("  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it");
        return Ok(());
    }
    spt_daemon::daemon::ensure_running().map_err(|e| format!("daemon start: {e}"))?;
    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline
    // (a `Whole` conn can't time out — nonblocking is dead on Windows named
    // pipes), so the single-threaded pump interleaves stdin + broker events
    // without wedging. The generous io_timeout bounds the setup calls (dial,
    // sessions, attach-open ack); the pump itself passes its own short slice.
    let mut brain = Brain::cold_start_pump(
        &spt_daemon::endpoint::broker_socket_name(),
        now_ms(),
        Duration::from_secs(10),
    )
    .map_err(|e| format!("broker connect: {e}"))?;

    let session_id = resolve_session(&mut brain, endpoint_id)
        .map_err(|e| format!("list sessions: {e}"))?
        .ok_or_else(|| format!("no live session for endpoint '{endpoint_id}'"))?;

    let ops = OpMinter::new();
    // Transport: a broker-minted in-process LOOPBACK connection (M12 W1.5, B1).
    // A QUIC self-dial is refused by iroh ("Connecting to ourself is not
    // supported"), so local attach cannot ride a self-dialed conn; instead the
    // broker wires an in-process loopback conn whose streams surface as the SAME
    // net-stream events the cross-node QUIC path produces. From `request_attach`
    // down this is byte-identical to a cross-node attach — the one-pump invariant
    // lives at the transport leaf inside the broker, never here. The loopback
    // peer's origin is this node's own id, so the target's serve gates same-node.
    let dialed = brain
        .net_dial_loopback()
        .map_err(|e| format!("loopback dial: {e}"))?;
    let stream_id = request_attach(&mut brain, dialed.conn_id, session_id, 0, ops.next(), intent)
        .map_err(|e| format!("attach request: {e}"))?;
    // Subscribe our side of the stream so the target's `Output` records surface
    // as `NetStreamData` events (the open + Request do not auto-subscribe).
    brain
        .net_stream_subscribe(stream_id, 0)
        .map_err(|e| format!("stream subscribe: {e}"))?;


Insertion point + online signal confirmed (`STATUS_ONLINE` via `read_info().status`). The fix: between the `if start` early-return and `run_attach`, poll the perch status to online (attach/view only), bounded deadline, timeout→err. doyle leaves the REQ to me — I'll **mint REQ-HAZARD-RC-ATTACH-ONLINE-RACE** (it's a bringup-core race, sibling to REQ-HAZARD-RC-ATTACH-FAILFAST, not picker-scoped). Read the sibling format + registry tail for placement:


577:id = "REQ-HAZARD-RC-ATTACH-FAILFAST"
578-[Omitted long context line]
579-[Omitted long context line]
580-[[requirements]]


Bash

tail -20 traceable-reqs.toml; echo "===WClines==="; grep -n "v0.13.0\|delivery-control\|W4\|W5" traceable-reqs.toml | tail -15

Show registry tail + v0.13.0 section markers


title = "An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W5 (repro-first, todlando). SCOPE landed = Gap B (no-session self-heal): reconcile_hosted_liveness, when it offlines a sessionless controllable perch (B2 keystone), ALSO clears a stale driven_by (set_driven_by None) — an OFFLINE endpoint can no longer render phantom CONTROLLED. RACE-FREE (no live session ⇒ no controller to re-stamp concurrently). impl = livehost.rs reconcile_hosted_liveness Gap-B clear + the additive SessionInfo.controller_by observability field (broker.rs KIND_SESSIONS). unit = pull_liveness unit extended (offlined sessionless perch clears driven_by; live/relay perches untouched). int = real-broker gates: driven_by_selfheal.rs gap_b (GREEN: reconcile offlines AND clears driven_by) + the A1/A2 characterization (inject_control_wedge.rs w5_a1/w5_a2). EMPIRICAL VERDICT (2026-06-19, real broker): controller_by==None is NOT a usable Gap-A signal — it is AMBIGUOUS (dispatch_spawn pre-attaches the spawner with by=None, so a LIVE LOCAL session also reads None) AND the production wedged-remote case (A2) keeps controller_by==Some(origin)+driven_by latched on an IDLE session (the broker still thinks it is controlled). A clean controller disconnect ALREADY self-heals via detach_if→clear_controller; W1 bounds the active-output wedge. The residual A2 (idle wedged REMOTE controller) needs BROKER-SIDE liveness eviction, NOT a brain reconcile controller_by read → DEFERRED to REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT. KNOWN-HAZARDS 7.15.

# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──
[[requirements]]
id = "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT"
title = "An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up)"
required_stages = []  # SEED — repro already exists (inject_control_wedge.rs w5_a2 characterization, under REQ-HAZARD-DRIVEN-BY-SELFHEAL int). doyle BOUNDARY RULING (2026-06-19): does NOT fold into W6 — W6 is the idle-DELIVERY consumer (translation binary injection), orthogonal to controller liveness; and a pump→clean-detach can't heal A2 either (A2 is a HALF-OPEN conn — remote gone, NO EOF — so serve_attach's read just blocks, no detach signal to propagate). Needs the broker-side D4c NetPresence oracle above. Schedule as a controller-liveness follow-up post W4/W6 (likely v0.13.x); doyle tracks. Activate [impl,unit,int] when the eviction lands.

# ── W7 (operator /diagnose 2026-06-19): rc Backspace key forwards the wrong byte ──
[[requirements]]
id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint)."

# ── W1b (operator /diagnose 2026-06-19, post-W1 dogfood escape): the effect-journal PTY-input wedge ──
[[requirements]]
id = "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE"
title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).
===WClines===
1202:title = "A bound endpoint's `info.cwd` is SET at bind so a freshly-created perch appears under its own project tab. ROOT (found, v0.13.0): `info.cwd` is NEVER set on bind — `cmd_bind` (spt-hosted) and `bind_from_seed` (harness-hosted) never thread cwd into `establish_perch`/`rec.cwd`. FIX: `cmd_bind` reads its own `current_dir` (the broker spawned it in `project_cwd`); `bind_from_seed` passes `seed.cwd` (already captured at seed time, currently DISCARDED). DISTINCT from REQ-PICKER-HISTORY-FRESH (v0.12.1) — that unioned cwd-origin into picker MEMBERSHIP but tested merge_origin_project with a PROVIDED origin; it never asserted `info.cwd` is actually set on bind, so a real `endpoint run` perch still had an empty cwd and the union had nothing to union. This is the v0.12.1 P1 'appears under its own project right away' claim that was REFUTED in the changelog — delivered for real here. (v0.13.0)"
1203:required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W3 (todlando). ROOT (doyle RCA): info.cwd NEVER set on bind. FIX: establish_perch gained a trailing cwd param + sets rec.cwd = cwd.or_else(prior.cwd) (revive carries forward); cmd_bind (spt-hosted) passes its own current_dir (broker spawned it in project_cwd); bind_from_seed (harness-hosted) passes seed.cwd (captured at api seed, previously DISCARDED). impl = api/startup.rs establish_perch/cmd_bind/bind_from_seed. unit = startup.rs bind_records_cwd_so_picker_can_group_by_project + revive_without_cwd_preserves_prior_cwd + seed_bind_records_seed_captured_cwd (mutation-verified — each guards a distinct branch). int = bind_cwd_project_e2e.rs (REAL detached daemon + endpoint run: live perch read_info().cwd is Some+non-empty AND appears under its project via project_id_for_dir membership). Delivers the v0.12.1 P1 that was REFUTED in the changelog. NOTE: cmd_bind stores raw current_dir (native slashes) while cmd_seed forward-slash-normalizes — slash-agnostic through project_id_for_dir, follow-up to unify.
1205:# ── W4: `spt endpoint run` picker UX ──
1208:title = "`spt endpoint run` picker UX (v0.13.0 operator dogfooding): (1) SKIP the first screen — open directly on 'Pick existing'; `n` jumps to 'Create new'. (2) AUTO-ATTACH after both Start-new AND Resume-from-history (both currently don't attach and show no stdout); add an `h` shortcut to run headless (no attach). (3) 'controlled by' shows the node NAME (node_label_display), not the raw hex. (4) Clean up Start-new output — drop the Rust `pid=Some(142748)` leak and the 'harness binds its perch on startup' internals; user-friendly, not a process log. (v0.13.0)"
1209:required_stages = []  # RETIRED/REDIRECTED (doyle W4 gate 2026-06-19): the W4 picker UX scope was DELIVERED as refinements of the EXISTING picker reqs, NOT under this id — fixes (1) open-on-Pick-existing, (2) attach-by-default + `h` headless, (4) terse bringup line landed under REQ-RUN-PICKER (v0.13.0-W4 extension); fix (3) controlled-by node-name landed under REQ-PICKER-4 (v0.13.0-W4 extension, the D item). This seed carries no evidence by design (folded, not duplicated) — kept as a redirect breadcrumb, not an open requirement. Live keypress path = operator HITL.
1211:# ── W5: driven_by self-heal (ONLINE+CONTROLLED clears without the detach IPC) ──
1214:title = "An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)"
1215:required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W5 (repro-first, todlando). SCOPE landed = Gap B (no-session self-heal): reconcile_hosted_liveness, when it offlines a sessionless controllable perch (B2 keystone), ALSO clears a stale driven_by (set_driven_by None) — an OFFLINE endpoint can no longer render phantom CONTROLLED. RACE-FREE (no live session ⇒ no controller to re-stamp concurrently). impl = livehost.rs reconcile_hosted_liveness Gap-B clear + the additive SessionInfo.controller_by observability field (broker.rs KIND_SESSIONS). unit = pull_liveness unit extended (offlined sessionless perch clears driven_by; live/relay perches untouched). int = real-broker gates: driven_by_selfheal.rs gap_b (GREEN: reconcile offlines AND clears driven_by) + the A1/A2 characterization (inject_control_wedge.rs w5_a1/w5_a2). EMPIRICAL VERDICT (2026-06-19, real broker): controller_by==None is NOT a usable Gap-A signal — it is AMBIGUOUS (dispatch_spawn pre-attaches the spawner with by=None, so a LIVE LOCAL session also reads None) AND the production wedged-remote case (A2) keeps controller_by==Some(origin)+driven_by latched on an IDLE session (the broker still thinks it is controlled). A clean controller disconnect ALREADY self-heals via detach_if→clear_controller; W1 bounds the active-output wedge. The residual A2 (idle wedged REMOTE controller) needs BROKER-SIDE liveness eviction, NOT a brain reconcile controller_by read → DEFERRED to REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT. KNOWN-HAZARDS 7.15.
1217:# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──
1220:title = "An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up)"
1221:required_stages = []  # SEED — repro already exists (inject_control_wedge.rs w5_a2 characterization, under REQ-HAZARD-DRIVEN-BY-SELFHEAL int). doyle BOUNDARY RULING (2026-06-19): does NOT fold into W6 — W6 is the idle-DELIVERY consumer (translation binary injection), orthogonal to controller liveness; and a pump→clean-detach can't heal A2 either (A2 is a HALF-OPEN conn — remote gone, NO EOF — so serve_attach's read just blocks, no detach signal to propagate). Needs the broker-side D4c NetPresence oracle above. Schedule as a controller-liveness follow-up post W4/W6 (likely v0.13.x); doyle tracks. Activate [impl,unit,int] when the eviction lands.
1226:title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
1227:required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint)."
1232:title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
1233:required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).


I have the registry conventions + insertion context. Let me read the exact EOF to append the new REQ block cleanly:


wc -l traceable-reqs.toml; echo "---last 3 lines---"; tail -3 traceable-reqs.toml | cut -c1-80

Get exact line count and EOF tail


1233 traceable-reqs.toml
---last 3 lines---
id = "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE"
title = "The effect journal serializes EVERY PTY effect under one mutex held ACR
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). F


Append the new REQ via Edit anchored on the unique EOF phrase:


Edit

Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).

 Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).

# ── bugs 3+4 (operator HITL 2026-06-19, doyle /diagnose, ONE root): endpoint-run attaches before online ──
[[requirements]]
id = "REQ-HAZARD-RC-ATTACH-ONLINE-RACE"
title = "`spt endpoint run` in an ATTACH/VIEW terminal action attaches BEFORE the freshly-spawned endpoint is online, so the attach races (or outright loses to) the harness bind. ROOT (doyle /diagnose, code-grounded): cmd_endpoint_run (cli.rs) does launch_harness_brokered_in -> (if start: return) -> run_attach with NO await-online between them. launch_harness_brokered_in returns once the harness PROCESS is spawned, but the broker-PTY bind (info status -> STATUS_ONLINE + the live session) lands ASYNC. Both picker attach paths route here with start=false (RunMode::Attach -> cmd_endpoint_run start=false,view=false): Start-now catches the endpoint mid-bringup -> run_attach attempts + loses the handshake race; Resume-from-history catches it still fully OFFLINE -> run_attach's status-gate (REQ-HAZARD-RC-ATTACH-FAILFAST) short-circuits 'offline - nothing to attach' and NEVER attempts. SAME root, two faces (the W4 attach-by-default surfaced both; an online endpoint is unaffected - the picker returns Outcome::Attach, not Run). FIX: in cmd_endpoint_run, when the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach - poll spt_store::info read_info().status to STATUS_ONLINE with a bounded harness-boot deadline (~25s) at a tight interval; on online -> run_attach; on timeout -> ENDPOINT_RUN_ONLINE_TIMEOUT err (do NOT attach a dead bringup). (v0.13.0)"
required_stages = ["impl", "int"]  # ACTIVATED v0.13.0 (todlando, doyle-dispatched bugs 3+4). impl = cmd_endpoint_run await_endpoint_online gate before run_attach on the attach/view path (start/headless return above, untouched). int = dummy_harness_e2e repro: a fresh `endpoint run` with NO --start (attach-by-default) CONNECTS to the live PTY post-bringup (RED pre-fix = the immediate attach bails 'offline - nothing to attach' / loses the race; GREEN post-fix = attached, PTY output observed). Repro-first (deterministic on the Resume/offline face). No unit: the await is a real-timing store-poll, integration-proven; the start-vs-attach branch is exercised by the int.


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

 Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).

# ── bugs 3+4 (operator HITL 2026-06-19, doyle /diagnose, ONE root): endpoint-run attaches before online ──
[[requirements]]
id = "REQ-HAZARD-RC-ATTACH-ONLINE-RACE"
title = "`spt endpoint run` in an ATTACH/VIEW terminal action attaches BEFORE the freshly-spawned endpoint is online, so the attach races (or outright loses to) the harness bind. ROOT (doyle /diagnose, code-grounded): cmd_endpoint_run (cli.rs) does launch_harness_brokered_in -> (if start: return) -> run_attach with NO await-online between them. launch_harness_brokered_in returns once the harness PROCESS is spawned, but the broker-PTY bind (info status -> STATUS_ONLINE + the live session) lands ASYNC. Both picker attach paths route here with start=false (RunMode::Attach -> cmd_endpoint_run start=false,view=false): Start-now catches the endpoint mid-bringup -> run_attach attempts + loses the handshake race; Resume-from-history catches it still fully OFFLINE -> run_attach's status-gate (REQ-HAZARD-RC-ATTACH-FAILFAST) short-circuits 'offline - nothing to attach' and NEVER attempts. SAME root, two faces (the W4 attach-by-default surfaced both; an online endpoint is unaffected - the picker returns Outcome::Attach, not Run). FIX: in cmd_endpoint_run, when the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach - poll spt_store::info read_info().status to STATUS_ONLINE with a bounded harness-boot deadline (~25s) at a tight interval; on online -> run_attach; on timeout -> ENDPOINT_RUN_ONLINE_TIMEOUT err (do NOT attach a dead bringup). (v0.13.0)"
required_stages = ["impl", "int"]  # ACTIVATED v0.13.0 (todlando, doyle-dispatched bugs 3+4). impl = cmd_endpoint_run await_endpoint_online gate before run_attach on the attach/view path (start/headless return above, untouched). int = dummy_harness_e2e repro: a fresh `endpoint run` with NO --start (attach-by-default) CONNECTS to the live PTY post-bringup (RED pre-fix = the immediate attach bails 'offline - nothing to attach' / loses the race; GREEN post-fix = attached, PTY output observed). Repro-first (deterministic on the Resume/offline face). No unit: the await is a real-timing store-poll, integration-proven; the start-vs-attach branch is exercised by the int.

title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"

required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).

+# ── bugs 3+4 (operator HITL 2026-06-19, doyle /diagnose, ONE root): endpoint-run attaches before online ──

+title = "`spt endpoint run` in an ATTACH/VIEW terminal action attaches BEFORE the freshly-spawned endpoint is online, so the attach races (or outright loses to) the harness bind. ROOT (doyle /diagnose, code-grounded): cmd_endpoint_run (cli.rs) does launch_harness_brokered_in -> (if start: return) -> run_attach with NO await-online between them. launch_harness_brokered_in returns once the harness PROCESS is spawned, but the broker-PTY bind (info status -> STATUS_ONLINE + the live session) lands ASYNC. Both picker attach paths route here with start=false (RunMode::Attach -> cmd_endpoint_run start=false,view=false): Start-now catches the endpoint mid-bringup -> run_attach attempts + loses the handshake race; Resume-from-history catches it still fully OFFLINE -> run_attach's status-gate (REQ-HAZARD-RC-ATTACH-FAILFAST) short-circuits 'offline - nothing to attach' and NEVER attempts. SAME root, two faces (the W4 attach-by-default surfaced both; an online endpoint is unaffected - the picker returns Outcome::Attach, not Run). FIX: in cmd_endpoint_run, when the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach - poll spt_store::info read_info().status to STATUS_ONLINE with a bounded harness-boot deadline (~25s) at a tight interval; on online -> run_attach; on timeout -> ENDPOINT_RUN_ONLINE_TIMEOUT err (do NOT attach a dead bringup). (v0.13.0)"

+required_stages = ["impl", "int"]  # ACTIVATED v0.13.0 (todlando, doyle-dispatched bugs 3+4). impl = cmd_endpoint_run await_endpoint_online gate before run_attach on the attach/view path (start/headless return above, untouched). int = dummy_harness_e2e repro: a fresh `endpoint run` with NO --start (attach-by-default) CONNECTS to the live PTY post-bringup (RED pre-fix = the immediate attach bails 'offline - nothing to attach' / loses the race; GREEN post-fix = attached, PTY output observed). Repro-first (deterministic on the Resume/offline face). No unit: the await is a real-timing store-poll, integration-proven; the start-vs-attach branch is exercised by the int.


REQ minted (registry-first). Now dispatch engineer Phase 1 — write the dummy-harness repro int, confirm RED against current (unfixed) code. I hold the cli.rs fix until RED confirms (no collision: engineer owns the test file, I own cli.rs):


Agent

RED int: endpoint-run attach-online race

Prompt:
REPRO-FIRST regression int for REQ-HAZARD-RC-ATTACH-ONLINE-RACE (doyle root-caused). You write ONE new int test and confirm it is RED against the CURRENT (unfixed) code. Do NOT modify any non-test source — the fix lands in a later step.

THE BUG (doyle /diagnose, code-grounded): `spt endpoint run` in an ATTACH terminal action (no `--start`) attaches BEFORE the freshly-spawned endpoint is online. In `cmd_endpoint_run` (crates/spt/src/cli.rs ~1318→1358) the flow is `launch_harness_brokered_in` (returns once the harness PROCESS is spawned) → run_attach, with NO await-online between them. The broker-PTY bind (info status→STATUS_ONLINE + a live session) lands ASYNC, so an immediate `run_attach` loses the race: it either short-circuits "offline — nothing to attach" (the REQ-HAZARD-RC-ATTACH-FAILFAST status-gate in rc.rs run_attach) or hits "no live session for endpoint" — never connecting to the live PTY.

STUDY FIRST: `crates/spt/tests/dummy_harness_e2e.rs` end-to-end — it is your TEMPLATE and fixture. It registers the `dummyharness` adapter, sets an isolated SPT_HOME, runs `endpoint run --start` then a SEPARATE `rc` attach, and asserts the dummyharness HEARTBEAT output appears on the attached PTY. It also REAPS the daemon tree (broker daemon.pid + brain + harness) on exit — you MUST replicate that hygiene (a survivor locks target\debug\spt.exe → EPERM on the next CI checkout).

ADD: a sibling test (e.g. `fn endpoint_run_attach_awaits_online_before_attaching()`) tagged `// [int->REQ-HAZARD-RC-ATTACH-ONLINE-RACE]`. It must:
1. Reuse the dummyharness registration + isolated SPT_HOME fixture from the existing test (factor/copy its setup helpers).
2. Run `spt endpoint run --adapter dummyharness --id <fresh-id>` WITHOUT `--start` (this is the attach-by-default path = the bug surface). Because attach BLOCKS pumping the live PTY, run it BOUNDED (mirror the existing `output_bounded`/timeout helper, ~30s) and capture stdout+stderr.
3. THE CLAIM: assert the captured output shows the dummyharness HEARTBEAT (the same live-PTY marker the existing test asserts after `rc` attach) = proof the attach CONNECTED to the live session post-bringup. Pre-fix this FAILS: the output instead shows the offline/no-live-session bail (e.g. "offline — nothing to attach" / "no live session") and NO heartbeat → RED.
4. HYGIENE (non-negotiable): reap the ENTIRE tree before returning EXACTLY like the existing test — graceful `daemon stop` best-effort, then taskkill /F /T the broker (daemon.pid) + belt the brain + the harness pid, poll-verify reaped. Compute the assert into a bool, do the full reap, THEN assert (so a RED still cleans up — never leave a stray spt.exe).

RUN IT: `cargo test -p spt --test dummy_harness_e2e endpoint_run_attach_awaits_online_before_attaching -- --nocapture` against current code. EXPECT RED (no heartbeat — the immediate attach bailed/raced). Capture the failure + any diagnostic.

DISCIPLINE: do NOT run bare `cargo test` across the workspace (runner-contract hazard); scope to `-p spt --test dummy_harness_e2e`. This test DOES spawn a real daemon+harness by design — allowed for THIS targeted invocation. No Monitor. Cap 4 iterations. If the dummyharness won't bind in your environment, STOP and report — do NOT fake a result.

REPORT: the full test fn (verbatim), the run result (RED with the assertion message + what the output showed instead of the heartbeat), and confirm you touched ONLY dummy_harness_e2e.rs.


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>