    /// The node currently controlling this endpoint's session (its broker-owned
    /// `driven_by`), if any — drives the W2.5 status-conditional confirm options
    /// (REQ-RCVIEW-1 / REQ-KICK-1): a CONTROLLED endpoint offers View + Kick, not
    /// a plain Attach. Local rows read it off `info.json`; subnet rows leave it
    /// `None` (the wire busy-refuse + client guidance still protect a remote one).
    pub driven_by: Option<String>,

    /// The node currently controlling this endpoint's session (its broker-owned
    /// `driven_by`), if any — drives the W2.5 status-conditional confirm options
    /// (REQ-RCVIEW-1 / REQ-KICK-1): a CONTROLLED endpoint offers View + Kick, not
    /// a plain Attach. Local rows read it off `info.json` and render it through the
    /// canonical `node_label_display` so the "controlled by" pin shows the NODE NAME
    /// (`LABEL (keyprefix…)`), not raw key-hex (REQ-PICKER-4); subnet rows leave it
    /// `None` (the wire busy-refuse + client guidance still protect a remote one).
    /// Only ever read as `is_some()` (the controlled gate) or rendered verbatim.
    pub driven_by: Option<String>,

-    /// a plain Attach. Local rows read it off `info.json`; subnet rows leave it

+    /// a plain Attach. Local rows read it off `info.json` and render it through the

+    /// canonical `node_label_display` so the "controlled by" pin shows the NODE NAME

+    /// (`LABEL (keyprefix…)`), not raw key-hex (REQ-PICKER-4); subnet rows leave it

+    /// Only ever read as `is_some()` (the controlled gate) or rendered verbatim.

/// Read-only viewers currently attached (broker-owned `viewer_count`), for the

/// `controlled by <node> (+N viewing)` pin. `0` ⇒ none.


Item 4 — clean the start output in cli.rs. Drop the `Some(...)` debug + prose internals; keep pid as a terse machine token (2 e2e tests parse it):


Edit

        Ok(spawned) => {
            eprintln!(
                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
                 (harness binds its perch on startup)",
                spawned.pid
            );
        }

        Ok(spawned) => {
            // A TERSE machine line (no Rust `Some(..)` Debug leak, no "binds its
            // perch on startup" internals — W4 UX: user-friendly, not a process
            // log). `pid=<n>` stays a parseable machine token (scripts + e2e read
            // 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}");
        }


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)

        Ok(spawned) => {
            eprintln!(
                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
                 (harness binds its perch on startup)",
                spawned.pid
            );
        }

        Ok(spawned) => {
            // A TERSE machine line (no Rust `Some(..)` Debug leak, no "binds its
            // perch on startup" internals — W4 UX: user-friendly, not a process
            // log). `pid=<n>` stays a parseable machine token (scripts + e2e read
            // 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}");
        }

-                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \

-                 (harness binds its perch on startup)",

+            // A TERSE machine line (no Rust `Some(..)` Debug leak, no "binds its

+            // perch on startup" internals — W4 UX: user-friendly, not a process

+            // log). `pid=<n>` stays a parseable machine token (scripts + e2e read

+            // the long-lived PTY child off it); a backend without a pid prints `-`.

+                .map(|p| p.to_string())

+                .unwrap_or_else(|| "-".to_string());

+            eprintln!("ENDPOINT_RUN:{id} adapter={adapter} session={session_id} pid={pid_display}");


Now update the 2 e2e parsers from `pid=Some(` to the new terse `pid=` form:


    // The broker-spawned harness pid (from `ENDPOINT_RUN:… pid=Some(NNN)`) — the
    // long-lived PTY child itself, NOT the transient `spt api bind` subprocess that
    // stamps info.json's pid. It must stay alive (heartbeating).
    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
    let harness_pid: Option<u32> = run_stderr
        .split("pid=Some(")
        .nth(1)
        .and_then(|s| s.split(')').next())
        .and_then(|s| s.trim().parse().ok());

    // The broker-spawned harness pid (from the terse `ENDPOINT_RUN:… pid=NNN`
    // machine token) — the long-lived PTY child itself, NOT the transient `spt api
    // bind` subprocess that stamps info.json's pid. It must stay alive (heartbeating).
    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
    let harness_pid: Option<u32> = run_stderr
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());


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

    // The broker-spawned harness pid (from `ENDPOINT_RUN:… pid=Some(NNN)`) — the
    // long-lived PTY child itself, NOT the transient `spt api bind` subprocess that
    // stamps info.json's pid. It must stay alive (heartbeating).
    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
    let harness_pid: Option<u32> = run_stderr
        .split("pid=Some(")
        .nth(1)
        .and_then(|s| s.split(')').next())
        .and_then(|s| s.trim().parse().ok());

    // The broker-spawned harness pid (from the terse `ENDPOINT_RUN:… pid=NNN`
    // machine token) — the long-lived PTY child itself, NOT the transient `spt api
    // bind` subprocess that stamps info.json's pid. It must stay alive (heartbeating).
    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
    let harness_pid: Option<u32> = run_stderr
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());

-    // The broker-spawned harness pid (from `ENDPOINT_RUN:… pid=Some(NNN)`) — the

-    // long-lived PTY child itself, NOT the transient `spt api bind` subprocess that

-    // stamps info.json's pid. It must stay alive (heartbeating).

+    // The broker-spawned harness pid (from the terse `ENDPOINT_RUN:… pid=NNN`

+    // machine token) — the long-lived PTY child itself, NOT the transient `spt api

+    // bind` subprocess that stamps info.json's pid. It must stay alive (heartbeating).

-        .and_then(|s| s.split(')').next())

+        .and_then(|s| s.split_whitespace().next())


82
83	/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
84	/// the captured output and the broker-spawned harness pid (parsed from the
85	/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).
86	fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
87	    let mut cmd = Command::new(spt_bin);
88	    cmd.no_window()
89	        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
90	        .env("SPT_HOME", home);
91	    let out = output_bounded(cmd, Duration::from_secs(45));
92	    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
93	    let pid = stderr
94	        .split("pid=Some(")
95	        .nth(1)
96	        .and_then(|s| s.split(')').next())
97	        .and_then(|s| s.trim().parse().ok());
98	    (out, pid)
99	}
100
101	/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.

/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
/// the captured output and the broker-spawned harness pid (parsed from the
/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).
fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
    let mut cmd = Command::new(spt_bin);
    cmd.no_window()
        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
        .env("SPT_HOME", home);
    let out = output_bounded(cmd, Duration::from_secs(45));
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    let pid = stderr
        .split("pid=Some(")
        .nth(1)
        .and_then(|s| s.split(')').next())
        .and_then(|s| s.trim().parse().ok());
    (out, pid)
}

/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.


/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
/// the captured output and the broker-spawned harness pid (parsed from the
/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).
fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
    let mut cmd = Command::new(spt_bin);
    cmd.no_window()
        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
        .env("SPT_HOME", home);
    let out = output_bounded(cmd, Duration::from_secs(45));
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    let pid = stderr
        .split("pid=Some(")
        .nth(1)
        .and_then(|s| s.split(')').next())
        .and_then(|s| s.trim().parse().ok());
    (out, pid)
}

/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
/// the captured output and the broker-spawned harness pid (parsed from the terse
/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).
fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
    let mut cmd = Command::new(spt_bin);
    cmd.no_window()
        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
        .env("SPT_HOME", home);
    let out = output_bounded(cmd, Duration::from_secs(45));
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    let pid = stderr
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());
    (out, pid)
}


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

/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
/// the captured output and the broker-spawned harness pid (parsed from the
/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).
fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
    let mut cmd = Command::new(spt_bin);
    cmd.no_window()
        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
        .env("SPT_HOME", home);
    let out = output_bounded(cmd, Duration::from_secs(45));
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    let pid = stderr
        .split("pid=Some(")
        .nth(1)
        .and_then(|s| s.split(')').next())
        .and_then(|s| s.trim().parse().ok());
    (out, pid)
}

/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
/// the captured output and the broker-spawned harness pid (parsed from the terse
/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).
fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
    let mut cmd = Command::new(spt_bin);
    cmd.no_window()
        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
        .env("SPT_HOME", home);
    let out = output_bounded(cmd, Duration::from_secs(45));
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    let pid = stderr
        .split("pid=")
        .nth(1)
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.trim().parse().ok());
    (out, pid)
}

-/// the captured output and the broker-spawned harness pid (parsed from the

-/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).

+/// the captured output and the broker-spawned harness pid (parsed from the terse

+/// `ENDPOINT_RUN:… pid=NNN` stderr machine token — the long-lived PTY child).


Item 2 watch-out: add `h` to the legend. The legend is screen-global; add confirm/resume-specific legends in view.rs:


/// The pinned keybind legend (bottom row). Designed so a future tap-mode can
/// surface the same actions without the keys (the phone caveat).
const LEGEND: &str = "←→ category · ↑↓ item · enter confirm · esc back · / filter · n new · s shortcut";

/// The pinned keybind legend (bottom row). Designed so a future tap-mode can
/// surface the same actions without the keys (the phone caveat).
const LEGEND: &str = "←→ category · ↑↓ item · enter confirm · esc back · / filter · n new · s shortcut";
/// The Confirm-screen legend — Enter ATTACHES, `h` is the headless escape (W4 UX).
const LEGEND_CONFIRM: &str = "↑↓ option · enter confirm · h headless · esc back · s shortcut";
/// The Resume-screen legend — Enter resumes + attaches, `h` resumes headless.
const LEGEND_RESUME: &str = "↑↓ session · enter resume · h headless · esc back";


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

/// The pinned keybind legend (bottom row). Designed so a future tap-mode can
/// surface the same actions without the keys (the phone caveat).
const LEGEND: &str = "←→ category · ↑↓ item · enter confirm · esc back · / filter · n new · s shortcut";
/// The Confirm-screen legend — Enter ATTACHES, `h` is the headless escape (W4 UX).
const LEGEND_CONFIRM: &str = "↑↓ option · enter confirm · h headless · esc back · s shortcut";
/// The Resume-screen legend — Enter resumes + attaches, `h` resumes headless.
const LEGEND_RESUME: &str = "↑↓ session · enter resume · h headless · esc back";

+/// The Confirm-screen legend — Enter ATTACHES, `h` is the headless escape (W4 UX).

+const LEGEND_CONFIRM: &str = "↑↓ option · enter confirm · h headless · esc back · s shortcut";

+/// The Resume-screen legend — Enter resumes + attaches, `h` resumes headless.

+const LEGEND_RESUME: &str = "↑↓ session · enter resume · h headless · esc back";

/// Highlight style for the row under a cursor.


fn legend() -> Paragraph<'static> {
    Paragraph::new(Line::from(Span::styled(
        LEGEND,
        Style::default().fg(Color::DarkGray),
    )))
}

/// The bottom row: a transient flash message (e.g. shortcut written) when set,
/// else the pinned keybind legend.
fn bottom(model: &PickerModel) -> Paragraph<'static> {
    match &model.flash {
        Some(msg) => Paragraph::new(Line::from(Span::styled(
            msg.clone(),
            Style::default().fg(Color::Yellow),
        ))),
        None => legend(),
    }
}

fn legend_text(text: &'static str) -> Paragraph<'static> {
    Paragraph::new(Line::from(Span::styled(
        text,
        Style::default().fg(Color::DarkGray),
    )))
}

fn legend() -> Paragraph<'static> {
    legend_text(LEGEND)
}

/// The bottom row: a transient flash message (e.g. shortcut written) when set,
/// else the given screen's keybind legend.
fn bottom(model: &PickerModel, legend: &'static str) -> Paragraph<'static> {
    match &model.flash {
        Some(msg) => Paragraph::new(Line::from(Span::styled(
            msg.clone(),
            Style::default().fg(Color::Yellow),
        ))),
        None => legend_text(legend),
    }
}


fn legend() -> Paragraph<'static> {
    Paragraph::new(Line::from(Span::styled(
        LEGEND,
        Style::default().fg(Color::DarkGray),
    )))
}

/// The bottom row: a transient flash message (e.g. shortcut written) when set,
/// else the pinned keybind legend.
fn bottom(model: &PickerModel) -> Paragraph<'static> {
    match &model.flash {
        Some(msg) => Paragraph::new(Line::from(Span::styled(
            msg.clone(),
            Style::default().fg(Color::Yellow),
        ))),
        None => legend(),
    }
}

fn legend_text(text: &'static str) -> Paragraph<'static> {
    Paragraph::new(Line::from(Span::styled(
        text,
        Style::default().fg(Color::DarkGray),
    )))
}

fn legend() -> Paragraph<'static> {
    legend_text(LEGEND)
}

/// The bottom row: a transient flash message (e.g. shortcut written) when set,
/// else the given screen's keybind legend.
fn bottom(model: &PickerModel, legend: &'static str) -> Paragraph<'static> {
    match &model.flash {
        Some(msg) => Paragraph::new(Line::from(Span::styled(
            msg.clone(),
            Style::default().fg(Color::Yellow),
        ))),
        None => legend_text(legend),
    }
}

+fn legend_text(text: &'static str) -> Paragraph<'static> {

-fn bottom(model: &PickerModel) -> Paragraph<'static> {

+/// else the given screen's keybind legend.

+fn bottom(model: &PickerModel, legend: &'static str) -> Paragraph<'static> {


    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
    f.render_widget(bottom(model), chunks[2]);
}

    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
    f.render_widget(bottom(model, LEGEND_CONFIRM), chunks[2]);
}


    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
    f.render_widget(bottom(model), chunks[2]);
}

    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
    f.render_widget(bottom(model, LEGEND_CONFIRM), chunks[2]);
}

.map(|(i, opt)| item(confirm_option_label(*opt).to_string(), i == model.confirm_cursor))

-    f.render_widget(bottom(model), chunks[2]);

+    f.render_widget(bottom(model, LEGEND_CONFIRM), chunks[2]);

// ── Resume-from-history ─────────────────────────────────────────────────────


    f.render_widget(
        List::new(items).block(titled_block("Resume from a prior session:")),
        chunks[0],
    );
    f.render_widget(legend(), chunks[1]);
}

    f.render_widget(
        List::new(items).block(titled_block("Resume from a prior session:")),
        chunks[0],
    );
    f.render_widget(legend_text(LEGEND_RESUME), chunks[1]);
}


    f.render_widget(
        List::new(items).block(titled_block("Resume from a prior session:")),
        chunks[0],
    );
    f.render_widget(legend(), chunks[1]);
}

    f.render_widget(
        List::new(items).block(titled_block("Resume from a prior session:")),
        chunks[0],
    );
    f.render_widget(legend_text(LEGEND_RESUME), chunks[1]);
}

-    f.render_widget(legend(), chunks[1]);

+    f.render_widget(legend_text(LEGEND_RESUME), chunks[1]);


All 4 src edits done. Now compile-check + run the picker unit tests before dispatching the engineer.


PowerShell

cargo test -p spt --lib picker:: 2>&1 | Select-Object -Last 40

Build + run picker lib unit tests


Error: Exit code 101
error: no library targets found in package `spt`


cargo test -p spt --bin spt picker:: 2>&1 | Select-Object -Last 45

Run picker unit tests in the spt bin


Error: Exit code 101
test picker::data::tests::origin_project_unions_into_membership ... ok
test picker::model::tests::prefill_jumps_to_create ... ok
test picker::model::tests::back_unwinds ... ok
test picker::model::tests::adapter_tree_nests_profiles ... ok
test picker::model::tests::display_status_four_state_type_gated ... ok
test picker::model::tests::confirm_options_status_dependent ... ok
test picker::model::tests::category_ring_wraps ... ok
test picker::model::tests::create_outcome_bakes_run ... ok
test picker::model::tests::kind_routes ... ok
test picker::model::tests::resume_outcome_bakes_session ... ok
test picker::model::tests::reenter_create_keeps_or_clears_id ... ok
test picker::model::tests::confirm_terminal_routes ... ok
test picker::model::tests::resume_title_render ... ok
test picker::model::tests::status_square_is_online_offline_only ... ok
test picker::model::tests::filter_narrows ... ok
test picker::data::tests::reconcile_overrides_subnet_with_local_roster ... ok
test picker::model::tests::visible_partitions_and_sorts ... ok
test picker::shortcut::tests::basename_is_parameterized ... ok
test picker::shortcut::tests::sentinel_detection ... ok
test picker::shortcut::tests::bakes_resume_and_action ... ok
test picker::shortcut::tests::bakes_create_attach_by_default ... ok
test picker::view::tests::adapter_tree_buffer ... ok
test picker::view::tests::resume_buffer_lists_sessions ... ok
test picker::view::tests::confirm_buffer_options ... ok
test picker::view::tests::kind_buffer ... FAILED
test picker::view::tests::pick_buffer_has_squares_and_description ... ok
test picker::view::tests::pick_empty_category_hint ... ok
test picker::view::tests::pick_status_line_four_state ... ok
test picker::shortcut::tests::write_create_update_refuse ... ok

failures:

---- picker::view::tests::kind_buffer stdout ----

thread 'picker::view::tests::kind_buffer' (162148) panicked at crates\spt\src\picker\view.rs:497:9:
assertion failed: s.contains("What kind of endpoint?")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    picker::view::tests::kind_buffer

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

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


Predicted: only `kind_buffer` fails (entry is now PickExisting). Fix it — set the Kind jump-target explicitly:


Edit

    #[test]
    fn kind_buffer() {
        let m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);
        let s = rendered(&m);
        assert!(s.contains("What kind of endpoint?"));

    #[test]
    fn kind_buffer() {
        // The Kind layer is now a jump-target (W4 opens on Pick-existing); render it
        // explicitly to assert its two choices.
        let mut m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);
        m.screen = Screen::Kind;
        let s = rendered(&m);
        assert!(s.contains("What kind of endpoint?"));


    #[test]
    fn kind_buffer() {
        let m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);
        let s = rendered(&m);
        assert!(s.contains("What kind of endpoint?"));

    #[test]
    fn kind_buffer() {
        // The Kind layer is now a jump-target (W4 opens on Pick-existing); render it
        // explicitly to assert its two choices.
        let mut m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);
        m.screen = Screen::Kind;
        let s = rendered(&m);
        assert!(s.contains("What kind of endpoint?"));

-        let m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);

+        // The Kind layer is now a jump-target (W4 opens on Pick-existing); render it

+        // explicitly to assert its two choices.

+        let mut m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);


PowerShell

cargo test -p spt --bin spt picker:: 2>&1 | Select-Object -Last 6

Re-run picker unit tests


test picker::view::tests::pick_empty_category_hint ... ok
test picker::view::tests::pick_buffer_has_squares_and_description ... ok
test picker::view::tests::pick_status_line_four_state ... ok

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


All 30 picker tests green. The 4 fixes + breaking-test fixes are in. Now add dated REQ notes (rule 3 traceability) for the W4 extensions:


Edit

required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1)

required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1). v0.13.0-W4 UX refinements (todlando): (1) bare picker OPENS on Pick-existing (Kind = jump-target, `n` → create); (2) Start-now + Resume-from-history ATTACH by default (was detached/no-stdout) with an `h` headless escape (start, no attach); (4) the bringup stderr line is a terse machine token (dropped the Rust `Some(..)` debug + "binds its perch" internals). impl = model new() entry + confirm_terminal attach default + start_headless_outcome + resume_outcome(headless) + mod.rs h keybinds + view legends + cli.rs terse line; unit = kind_routes(PickExisting entry) + confirm_terminal_routes(Start→Attach, h→Start) + resume_outcome_bakes_session(headless). Live keypress path = HITL verify (no int).


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


495	id = "REQ-RUN-PICKER"
496	title = "Interactive `spt endpoint run` picker (ratatui TUI): bare `spt endpoint run` (no --adapter/--id) enters an in-process picker (flags-present = the REQ-HOST-RUN-1 non-interactive path, untouched). Layer 1 picks kind (Create new | Pick existing). Create-new: choose a registered kind=\"harness\" adapter with its shipped+local profiles tree-nested (registry::registered / manifest.profiles / local_profile_names) → enter a charset-validated id → start. Pick-existing: category select (left/right) over [<cwd-project> | Local node | Subnet], endpoints grouped + alphabetically sorted per category, a status square per endpoint (online green ■ / offline gray ▢ — the blue \"attached\" tri-state + Kick are DEFERRED to a broker attach-presence slice, M12-W2-RULING Q1), type-to-filter (`/`, nucleo-matcher), a pinned keybind legend, and a right-half two-pane description (harness adapter:profile · best-effort project history newest→oldest from the contextstore p-<project> branches, empty-if-none · `spt endpoint description`). Confirm layer offers status-dependent options — Attach/Start/View (rc pump / cmd_endpoint_run) · Instantiate-locally (remote) · Change-harness-adapter (offline) · Fork (cmd_fork) · Resume-from-history (offline+LOCAL only; enumerate spt_store::sessions::last_k, titles `<project> @ <ts> (…id5)`, feed session_id → cmd_endpoint_run --resume). A single action enum is the source of truth so a future tap-mode (phone PTY) layers on without re-coupling to keybinds. EVERY terminal action routes through cmd_endpoint_run / existing CLI fns — no second bringup path."
497	required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1)

id = "REQ-RUN-PICKER"
title = "Interactive `spt endpoint run` picker (ratatui TUI): bare `spt endpoint run` (no --adapter/--id) enters an in-process picker (flags-present = the REQ-HOST-RUN-1 non-interactive path, untouched). Layer 1 picks kind (Create new | Pick existing). Create-new: choose a registered kind=\"harness\" adapter with its shipped+local profiles tree-nested (registry::registered / manifest.profiles / local_profile_names) → enter a charset-validated id → start. Pick-existing: category select (left/right) over [<cwd-project> | Local node | Subnet], endpoints grouped + alphabetically sorted per category, a status square per endpoint (online green ■ / offline gray ▢ — the blue \"attached\" tri-state + Kick are DEFERRED to a broker attach-presence slice, M12-W2-RULING Q1), type-to-filter (`/`, nucleo-matcher), a pinned keybind legend, and a right-half two-pane description (harness adapter:profile · best-effort project history newest→oldest from the contextstore p-<project> branches, empty-if-none · `spt endpoint description`). Confirm layer offers status-dependent options — Attach/Start/View (rc pump / cmd_endpoint_run) · Instantiate-locally (remote) · Change-harness-adapter (offline) · Fork (cmd_fork) · Resume-from-history (offline+LOCAL only; enumerate spt_store::sessions::last_k, titles `<project> @ <ts> (…id5)`, feed session_id → cmd_endpoint_run --resume). A single action enum is the source of truth so a future tap-mode (phone PTY) layers on without re-coupling to keybinds. EVERY terminal action routes through cmd_endpoint_run / existing CLI fns — no second bringup path."
required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1)


required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1)

required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1). v0.13.0-W4 UX refinements (todlando 2026-06-19): (1) bare picker OPENS on Pick-existing (Kind = jump-target, `n` → create); (2) Start-now + Resume-from-history ATTACH by default (was detached/no-stdout) with an `h` headless escape (start, no attach); (4) the bringup stderr line is a terse machine token (dropped the Rust `Some(..)` debug + "binds its perch" internals). impl = model new() entry + confirm_terminal attach default + start_headless_outcome + resume_outcome(headless) + mod.rs h keybinds + view legends + cli.rs terse line; unit = kind_routes(PickExisting entry) + confirm_terminal_routes(Start→Attach, h→Start) + resume_outcome_bakes_session(headless). Live keypress path = HITL verify (no 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)

title = "Interactive `spt endpoint run` picker (ratatui TUI): bare `spt endpoint run` (no --adapter/--id) enters an in-process picker (flags-present = the REQ-HOST-RUN-1 non-interactive path, untouched). Layer 1 picks kind (Create new | Pick existing). Create-new: choose a registered kind=\"harness\" adapter with its shipped+local profiles tree-nested (registry::registered / manifest.profiles / local_profile_names) → enter a charset-validated id → start. Pick-existing: category select (left/right) over [<cwd-project> | Local node | Subnet], endpoints grouped + alphabetically sorted per category, a status square per endpoint (online green ■ / offline gray ▢ — the blue \"attached\" tri-state + Kick are DEFERRED to a broker attach-presence slice, M12-W2-RULING Q1), type-to-filter (`/`, nucleo-matcher), a pinned keybind legend, and a right-half two-pane description (harness adapter:profile · best-effort project history newest→oldest from the contextstore p-<project> branches, empty-if-none · `spt endpoint description`). Confirm layer offers status-dependent options — Attach/Start/View (rc pump / cmd_endpoint_run) · Instantiate-locally (remote) · Change-harness-adapter (offline) · Fork (cmd_fork) · Resume-from-history (offline+LOCAL only; enumerate spt_store::sessions::last_k, titles `<project> @ <ts> (…id5)`, feed session_id → cmd_endpoint_run --resume). A single action enum is the source of truth so a future tap-mode (phone PTY) layers on without re-coupling to keybinds. EVERY terminal action routes through cmd_endpoint_run / existing CLI fns — no second bringup path."

-required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1)

+required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1). v0.13.0-W4 UX refinements (todlando 2026-06-19): (1) bare picker OPENS on Pick-existing (Kind = jump-target, `n` → create); (2) Start-now + Resume-from-history ATTACH by default (was detached/no-stdout) with an `h` headless escape (start, no attach); (4) the bringup stderr line is a terse machine token (dropped the Rust `Some(..)` debug + "binds its perch" internals). impl = model new() entry + confirm_terminal attach default + start_headless_outcome + resume_outcome(headless) + mod.rs h keybinds + view legends + cli.rs terse line; unit = kind_routes(PickExisting entry) + confirm_terminal_routes(Start→Attach, h→Start) + resume_outcome_bakes_session(headless). Live keypress path = HITL verify (no int).

title = "`<basename>-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `<basename>-<id>` launcher at the project root baking the current selection's non-interactive `spt endpoint run` flags (terminal actions only: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). BASENAME IS A PARAMETER (operator rev. 2026-06-14): harness-agnostic spt-core defaults to `spt` (→ `spt-<id>`); an adapter/flow OVERRIDES it (spt-claude-code → `cc`), so spt-core NEVER bakes `cc` (a harness name) into itself. The basename must be a DISTINCT token, never bare `spt` (a `spt.cmd` would shadow the real `spt.exe` only under cmd.exe cwd-first search, silently no-op in PowerShell/Unix, and self-recurse). The script is the CURRENT OS's native form — `.cmd` on Windows (NOT `.ps1`: default PATHEXT excludes `.ps1` so a bare/ext-less name never resolves one; `.cmd` is PATHEXT-resolvable), POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). The generated header documents the invocation reality (cmd.exe bare `<name>` in the project dir / PowerShell `.\\<name>` / Unix `./<name>`; a truly-bare basename on PATH = a PATH-installed launcher, `/spt:setup`'s job). Overwrite is SENTINEL-guarded: the generator writes + checks a generated-by header marker — it overwrites its own prior output freely, but REFUSES + warns if a same-named file lacks the sentinel (never clobber a user file). Requires the additive `--create` flag on `Run{}` (the default-fresh made explicit; N-1-safe)."


543	id = "REQ-PICKER-4"
544	title = "The picker's Subnet category renders the canonical node LABEL, not bare key-hex: a subnet row's node renders as 'LABEL (keyprefix…)' (e.g. 'HFENDULEAM (bcead52b…)') per CONTEXT.md:650 + Instance.node_label, NOT the raw node key-hex (SPT_DEV:14efb80cb… — a picker-only regression because resource_projection→ResourceRow drops node_label, so data.rs subnet_rows uses the raw row.node). Thread node_label into the picker subnet path (ResourceRow gains node_label, or subnet_rows looks it up via the registry's node_labels) and REUSE the one canonical render (format!(\"{l} ({}…)\", key_prefix) — cli.rs / wansend.rs), never a re-implementation. (v0.10.0)"
545	required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-4: impl = node_label threaded onto ResourceRow in resource_projection (instance label, node_labels-map fallback) + canonical node_label_display reused by BOTH the picker and endpoint-list; unit = resource_projection_threads_node_label + node_label_display render

id = "REQ-PICKER-4"
title = "The picker's Subnet category renders the canonical node LABEL, not bare key-hex: a subnet row's node renders as 'LABEL (keyprefix…)' (e.g. 'HFENDULEAM (bcead52b…)') per CONTEXT.md:650 + Instance.node_label, NOT the raw node key-hex (SPT_DEV:14efb80cb… — a picker-only regression because resource_projection→ResourceRow drops node_label, so data.rs subnet_rows uses the raw row.node). Thread node_label into the picker subnet path (ResourceRow gains node_label, or subnet_rows looks it up via the registry's node_labels) and REUSE the one canonical render (format!(\"{l} ({}…)\", key_prefix) — cli.rs / wansend.rs), never a re-implementation. (v0.10.0)"
required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-4: impl = node_label threaded onto ResourceRow in resource_projection (instance label, node_labels-map fallback) + canonical node_label_display reused by BOTH the picker and endpoint-list; unit = resource_projection_threads_node_label + node_label_display render


required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-4: impl = node_label threaded onto ResourceRow in resource_projection (instance label, node_labels-map fallback) + canonical node_label_display reused by BOTH the picker and endpoint-list; unit = resource_projection_threads_node_label + node_label_display render

required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-4: impl = node_label threaded onto ResourceRow in resource_projection (instance label, node_labels-map fallback) + canonical node_label_display reused by BOTH the picker and endpoint-list; unit = resource_projection_threads_node_label + node_label_display render. v0.13.0-W4 extension (todlando 2026-06-19, the D item): a LOCAL row's `driven_by` controller key now renders the NODE NAME too — data.rs node_label_map() (subnet snapshots' node_labels lease) + driven_by_display() routes the key through the SAME node_label_display, so the "controlled by" pin shows `LABEL (keyprefix…)` not raw hex; degrades to `keyprefix…` when the label is unknown. impl = node_label_map + driven_by_display wired into local_rows; unit = driven_by_display (label present/absent/None).


title = "The picker's Subnet category renders the canonical node LABEL, not bare key-hex: a subnet row's node renders as 'LABEL (keyprefix…)' (e.g. 'HFENDULEAM (bcead52b…)') per CONTEXT.md:650 + Instance.node_label, NOT the raw node key-hex (SPT_DEV:14efb80cb… — a picker-only regression because resource_projection→ResourceRow drops node_label, so data.rs subnet_rows uses the raw row.node). Thread node_label into the picker subnet path (ResourceRow gains node_label, or subnet_rows looks it up via the registry's node_labels) and REUSE the one canonical render (format!(\"{l} ({}…)\", key_prefix) — cli.rs / wansend.rs), never a re-implementation. (v0.10.0)"

-required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-4: impl = node_label threaded onto ResourceRow in resource_projection (instance label, node_labels-map fallback) + canonical node_label_display reused by BOTH the picker and endpoint-list; unit = resource_projection_threads_node_label + node_label_display render

+required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-4: impl = node_label threaded onto ResourceRow in resource_projection (instance label, node_labels-map fallback) + canonical node_label_display reused by BOTH the picker and endpoint-list; unit = resource_projection_threads_node_label + node_label_display render. v0.13.0-W4 extension (todlando 2026-06-19, the D item): a LOCAL row's `driven_by` controller key now renders the NODE NAME too — data.rs node_label_map() (subnet snapshots' node_labels lease) + driven_by_display() routes the key through the SAME node_label_display, so the "controlled by" pin shows `LABEL (keyprefix…)` not raw hex; degrades to `keyprefix…` when the label is unknown. impl = node_label_map + driven_by_display wired into local_rows; unit = driven_by_display (label present/absent/None).

title = "`spt endpoint list` (bare/subnet view) renders an ALIGNED table with canonical node labels: cmd_endpoint_list prints subnet rows with `\\t` TAB separators (cli.rs:~1651-1662) so variable-width endpoint_ids snap fields to different tab-stops → a RAGGED status column (operator screenshot: X/help statuses misaligned vs rt-*/sptc-*/wall-a); and it calls the node renderer with no label → bare key-hex for every row (SAME ResourceRow-drops-node_label root as REQ-PICKER-4). FIX: max-width per-column padding (mirror render_node_rows' pad, pad by char count not byte len — '…' is multibyte) replacing the tabs, and render the node via the shared node_label_display now that ResourceRow carries node_label (REQ-PICKER-4). Extract a pure row-formatter seam so the alignment+label is unit-testable. ALSO: the bare list is the SUBNET view (a just-run LOCAL perch is invisible cross-subnet until the next advertise tick), so emit a `--local` hint line so a freshly-run endpoint isn't perceived as lost. (v0.10.0; operator-flagged + doyle dispatch 2026-06-17)"


Src + REQ notes complete, all picker tests green, no collision risk. Now dispatch ONE spt-test-engineer (background, ≤4 iter) to add the one remaining new-coverage seam — the `driven_by_display` pure helper for item 3:


Agent

Unit test driven_by_display helper

Prompt:
Add ONE focused unit test to the spt-core picker data layer. Do NOT edit any non-test code — all production code is already written and committed-ready; you ONLY add a test fn.

CONTEXT: W4 picker UX item 3 (REQ-PICKER-4 extension) added a pure helper in `crates/spt/src/picker/data.rs`:

```rust
fn driven_by_display(
    hex: Option<&str>,
    node_labels: &std::collections::BTreeMap<String, String>,
) -> Option<String> {
    hex.map(|h| {
        spt_net::net::registry::node_label_display(h, node_labels.get(h).map(|s| s.as_str()))
    })
}
```

`node_label_display(node, Some(label))` => `"{label} ({keyprefix}…)"`; `node_label_display(node, None)` => `"{keyprefix}…"`. (key_prefix truncates the hex.)

TASK: in the existing `#[cfg(test)] mod tests` block at the bottom of `crates/spt/src/picker/data.rs`, add ONE test `fn driven_by_display_renders_name_not_hex()` tagged `// [unit->REQ-PICKER-4]` immediately above it. Assert:
- `None` controller key in ⇒ `None` out.
- A key WITH a label in the map ⇒ output contains the label (the NODE NAME) and is NOT the bare full hex (use a long hex like "bcead52b1234deadbeef"; assert the result contains the label text e.g. "HFENDULEAM" and does NOT equal the full raw hex).
- A key with NO label in the map ⇒ output is the truncated `keyprefix…` form (contains '…', and is shorter than / not equal to the full raw hex).
Build the BTreeMap inline. Match the surrounding test style (the existing tests in that module are the pattern).

GATES you must pass before reporting done (run exactly these, report PASS/FAIL + tail output for each):
1. `cargo test -p spt --bin spt picker::data` — your new test + the existing data tests green.
2. `cargo clippy --workspace --all-targets -- -D warnings` — zero warnings.

Discipline: do NOT run `cargo test` bare across the workspace (it spawns the real daemon — runner-contract hazard); scope to `-p spt --bin spt`. Do NOT use Monitor. Cap at 4 iterations; if a gate won't pass after 4 tries, STOP and report the exact failure. Report: the test you added (verbatim), and each gate's PASS/FAIL.


<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>


Async agent launched successfully.
agentId: a308ebff4b65b8d94 (internal ID - do not mention to user. Use SendMessage with to: 'a308ebff4b65b8d94' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\a308ebff4b65b8d94.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

Unit test driven_by_display helper

Prompt:
Add ONE focused unit test to the spt-core picker data layer. Do NOT edit any non-test code — all production code is already written and committed-ready; you ONLY add a test fn.

CONTEXT: W4 picker UX item 3 (REQ-PICKER-4 extension) added a pure helper in `crates/spt/src/picker/data.rs`:

```rust
fn driven_by_display(
    hex: Option<&str>,
    node_labels: &std::collections::BTreeMap<String, String>,
) -> Option<String> {
    hex.map(|h| {
        spt_net::net::registry::node_label_display(h, node_labels.get(h).map(|s| s.as_str()))
    })
}
```

`node_label_display(node, Some(label))` => `"{label} ({keyprefix}…)"`; `node_label_display(node, None)` => `"{keyprefix}…"`. (key_prefix truncates the hex.)

TASK: in the existing `#[cfg(test)] mod tests` block at the bottom of `crates/spt/src/picker/data.rs`, add ONE test `fn driven_by_display_renders_name_not_hex()` tagged `// [unit->REQ-PICKER-4]` immediately above it. Assert:
- `None` controller key in ⇒ `None` out.
- A key WITH a label in the map ⇒ output contains the label (the NODE NAME) and is NOT the bare full hex (use a long hex like "bcead52b1234deadbeef"; assert the result contains the label text e.g. "HFENDULEAM" and does NOT equal the full raw hex).
- A key with NO label in the map ⇒ output is the truncated `keyprefix…` form (contains '…', and is shorter than / not equal to the full raw hex).
Build the BTreeMap inline. Match the surrounding test style (the existing tests in that module are the pattern).

GATES you must pass before reporting done (run exactly these, report PASS/FAIL + tail output for each):
1. `cargo test -p spt --bin spt picker::data` — your new test + the existing data tests green.
2. `cargo clippy --workspace --all-targets -- -D warnings` — zero warnings.

Discipline: do NOT run `cargo test` bare across the workspace (it spawns the real daemon — runner-contract hazard); scope to `-p spt --bin spt`. Do NOT use Monitor. Cap at 4 iterations; if a gate won't pass after 4 tries, STOP and report the exact failure. Report: the test you added (verbatim), and each gate's PASS/FAIL.


Engineer running in background. Meanwhile, run the cheap traceability gate (no cargo lock contention with the engineer):


PowerShell

traceable-reqs check 2>&1 | Select-Object -Last 25

Run traceability gate


<owl_messages>
<owl_message from="todlando-w176" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: todlando-w176
Agent type: spt-test-engineer
To message it: $OWL send todlando-w176 todlando</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.


       Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL.
  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).


Let me confirm the traceability exit code explicitly:


traceable-reqs check > $null 2>&1; "EXIT=$LASTEXITCODE"

Confirm traceability gate exit code


1	//! Picker **data gather** — read-only projections over the EXISTING surfaces
2	//! the rest of the CLI already uses. No new daemon surface, no new store
3	//! (doyle ruling): adapters from the registry, endpoints from the roster + the
4	//! WAN registry snapshots, descriptions from `info.json`, project history
5	//! best-effort from the context store, resume rows from the session ledger.
6	//!
7	//! Everything here degrades to empty rather than failing — the picker is a
8	//! front-end; a missing surface means "nothing to show", never a crash.
9	// [impl->REQ-RUN-PICKER]
10
11	use std::path::Path;
12
13	use spt_store::perch::{self, ParentHint};
14	use spt_store::{info, project, sessions};
15
16	use super::model::{AdapterInfo, AdapterOption, EndpointRow, EpStatus, ResumeRow};
17
18	/// The cwd project name — the Project category label + the project-history match
19	/// key. Derived exactly like the daemon derives it (remote-url slug → toplevel
20	/// folder → dir name).
21	pub fn project_name() -> String {
22	    let dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
23	    project::project_id_for_dir(&dir)
24	}
25
26	/// The selectable adapter tree: every active `kind="harness"` adapter with its
27	/// shipped + local profiles nested under it.
28	pub fn adapter_options() -> Vec<AdapterOption> {
29	    let adapters_dir = perch::adapters_dir();
30	    let mut infos: Vec<AdapterInfo> = Vec::new();
31	    for (record, manifest) in spt_runtime::registry::registered(&adapters_dir) {
32	        if manifest.adapter.kind != spt_runtime::manifest::AdapterKind::Harness {
33	            continue;
34	        }
35	        let mut shipped: Vec<String> = manifest.profiles.keys().cloned().collect();
36	        shipped.sort();
37	        let local = spt_runtime::registry::local_profile_names(&adapters_dir, &record.name);
38	        infos.push(AdapterInfo {
39	            name: record.name,
40	            shipped,
41	            local,
42	        });
43	    }
44	    infos.sort_by(|a, b| a.name.cmp(&b.name));
45	    super::model::build_adapter_tree(&infos)
46	}
47
48	/// The shortcut basename an adapter option declares in its RESOLVED manifest
49	/// (`[adapter] shortcut_basename`, REQ-MANIFEST-7), defaulting to the
50	/// harness-agnostic [`DEFAULT_BASENAME`](super::shortcut::DEFAULT_BASENAME)
51	/// (`spt`) when absent or the adapter is unresolvable. This is the seam that
52	/// lets spt-claude-code brand its shortcuts `cc-<id>` without spt-core ever
53	/// hardcoding a harness name.
54	// [impl->REQ-MANIFEST-7]
55	pub fn shortcut_basename(adapter_option: &str) -> String {
56	    let adapters_dir = perch::adapters_dir();
57	    spt_runtime::registry::resolve_option(&adapters_dir, adapter_option)
58	        .ok()
59	        .and_then(|(_, m)| m.adapter.shortcut_basename)
60	        .filter(|b| !b.trim().is_empty())
61	        .unwrap_or_else(|| super::shortcut::DEFAULT_BASENAME.to_string())
62	}
63
64	/// Every discoverable endpoint, across all three categories. The model
65	/// partitions by category; this just produces the union (local perches +
66	/// subnet registry rows).
67	// [impl->REQ-RUN-PICKER]
68	pub fn gather_endpoints() -> Vec<EndpointRow> {
69	    // Resolve the node-key→label map ONCE (the subnet registries) so a LOCAL row's
70	    // `driven_by` controller renders as a NAME, not bare key-hex (REQ-PICKER-4).
71	    let node_labels = node_label_map();
72	    let local = local_rows(&node_labels);
73	    let subnet = subnet_rows();
74	    let mut rows = Vec::with_capacity(local.len() + subnet.len());
75	    rows.extend(local);
76	    rows.extend(subnet);
77	    reconcile_self_owned(&mut rows);
78	    rows
79	}
80
81	/// The known node-key → human label map, gathered from this node's per-subnet
82	/// registry snapshots (the same `node_labels` lease `endpoint list` reads). Used
83	/// to render a `driven_by` controller key as its canonical `LABEL (keyprefix…)`
84	/// instead of bare hex (REQ-PICKER-4). Degrades to empty (→ bare `keyprefix…`,
85	/// still better than the full raw key) when no snapshot carries the label.
86	fn node_label_map() -> std::collections::BTreeMap<String, String> {
87	    let mut map = std::collections::BTreeMap::new();
88	    let subnets = spt_store::subnet::SubnetStore::load();
89	    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
90	    for s in &subnets.subnets {
91	        if let Some(reg) = regs.get(&s.name) {
92	            for (node, label) in reg.node_labels() {
93	                map.entry(node.to_string()).or_insert_with(|| label.to_string());
94	            }
95	        }
96	    }
97	    map
98	}
99
100	/// Render an endpoint's raw `driven_by` controller key through the ONE canonical
101	/// node renderer (`node_label_display`) — `LABEL (keyprefix…)` when the label is
102	/// known, else `keyprefix…` — so the picker's "controlled by" pin never leaks the
103	/// full raw key-hex (REQ-PICKER-4, the D item). Pure over its inputs (unit-tested);
104	/// `None` in ⇒ `None` out (no controller).
105	// [impl->REQ-PICKER-4]
106	fn driven_by_display(
107	    hex: Option<&str>,
108	    node_labels: &std::collections::BTreeMap<String, String>,
109	) -> Option<String> {
110	    hex.map(|h| {
111	        spt_net::net::registry::node_label_display(h, node_labels.get(h).map(|s| s.as_str()))
112	    })
113	}
114
115	/// Reconcile the status of a self-owned endpoint that is dual-listed in both the
116	/// Local and Subnet categories (REQ-PICKER-3). The two listings draw from
117	/// independent sources — Local from the live roster (ground truth for an endpoint
118	/// THIS node hosts), Subnet from the persisted WAN snapshot (a lagged projection)
119	/// — so a stale snapshot can disagree with the roster and paint the SAME endpoint
120	/// online in one category and offline in the other (defect 3). The roster wins:
121	/// for any subnet row whose id matches a local (is_local) row, override the
122	/// status/controllable/endpoint_type with the local fact. Both listings are kept
123	/// (distinct views); only the status square is unified.
124	// [impl->REQ-PICKER-3]
125	fn reconcile_self_owned(rows: &mut [EndpointRow]) {
126	    use std::collections::HashMap;
127	    let local: HashMap<String, (EpStatus, Option<bool>, String)> = rows
128	        .iter()
129	        .filter(|r| r.is_local)
130	        .map(|r| (r.id.clone(), (r.status, r.controllable, r.endpoint_type.clone())))
131	        .collect();
132	    for row in rows.iter_mut() {
133	        if row.is_local {
134	            continue;
135	        }
136	        if let Some((status, controllable, endpoint_type)) = local.get(&row.id) {
137	            row.status = *status;
138	            row.controllable = *controllable;
139	            row.endpoint_type = endpoint_type.clone();
140	        }
141	    }
142	}
143
144	/// This node's local perches (from the roster). Local rows feed both the Local
145	/// and Project categories (the model filters Project by project-history match).
146	/// `node_labels` resolves a row's `driven_by` controller key to its display name.
147	fn local_rows(node_labels: &std::collections::BTreeMap<String, String>) -> Vec<EndpointRow> {
148	    crate::roster::enumerate()
149	        .into_iter()
150	        .map(|p| {
151	            let perch_path = perch::resolve_perch_path(&p.id, ParentHint::Infer);
152	            let rec = info::read_info(&perch_path);
153	            let status = if p.alive {
154	                EpStatus::Online
155	            } else {
156	                EpStatus::Offline
157	            };
158	            let adapter_profile = rec
159	                .as_ref()
160	                .and_then(|r| r.adapter.clone())
161	                .unwrap_or_else(|| "(unknown adapter)".to_string());
162	            let description = rec
163	                .as_ref()
164	                .and_then(|r| r.resources.clone())
165	                .unwrap_or_default();
166	            // Project membership = committed-context projects (history) UNION the
167	            // endpoint's ORIGIN project (the cwd it was created in). A FRESH local

... [283 lines truncated] ...


Traceability gate EXIT=0. The engineer has already landed its test in data.rs and is running its gates (workspace clippy + data tests). Waiting on its completion before the final real-bin build to avoid target-lock contention.

**W4 picker UX — 4 fixes done (src + REQ notes + breaking-test fixes):**

1. **Open on Pick-existing** — `model.rs` `new()`: bare picker entry `Kind`→`PickExisting`; Kind now a jump-target (Esc backs to it, `n` → create). Already-wired `n` keybind + `back()` graph unchanged.
2. **Auto-attach + `h` headless** — offline `Start now` + `Resume-from-history` now ATTACH by default (`confirm_terminal` Start→`RunMode::Attach`, `resume_outcome(headless)`); new `start_headless_outcome()` + `h` keybind on Confirm/Resume screens for detached start; legends updated (`LEGEND_CONFIRM`/`LEGEND_RESUME` add `h headless`).
3. **`controlled by` shows node NAME** — `data.rs` `node_label_map()` + `driven_by_display()` route a LOCAL row's `driven_by` key through the canonical `node_label_display` → `LABEL (keyprefix…)`, not raw hex. `[impl->REQ-PICKER-4]`.
4. **Clean start output** — `cli.rs` bringup line is now a terse machine token (dropped Rust `Some(..)` debug + "binds its perch on startup" prose); `pid=<n>` stays parseable; updated the 2 e2e parsers (`pid=Some(`→`pid=`).

**Gates so far:** 30/30 picker unit tests green · `traceable-reqs check` EXIT=0. Pending: engineer's workspace clippy + the real `spt` bin rebuild. doyle gates after.


<task-notification>
<task-id>a308ebff4b65b8d94</task-id>
<tool-use-id>toolu_01FUpyysbnJHBkc4sNodNF7h</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\a308ebff4b65b8d94.output</output-file>
<status>completed</status>
<summary>Agent "Unit test driven_by_display helper" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Both gates pass. Done.

## Summary

Added ONE focused unit test to the picker data layer. No non-test code was touched.

**File:** `C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\data.rs` (in the existing `#[cfg(test)] mod tests` block).

**Test added (verbatim):**

```rust
// [unit->REQ-PICKER-4] the `driven_by` controller pin renders the node NAME,
// never the bare raw key-hex: `None` controller ⇒ `None`; a key WITH a known
// label ⇒ "LABEL (keyprefix…)" (contains the name, is not the full hex); a key
// with NO label ⇒ the truncated "keyprefix…" form (still not the full hex).
#[test]
fn driven_by_display_renders_name_not_hex() {
    let full_hex = "bcead52b1234deadbeef";
    let label = "HFENDULEAM";
    let mut node_labels = std::collections::BTreeMap::new();
    node_labels.insert(full_hex.to_string(), label.to_string());

    // No controller ⇒ no display.
    assert_eq!(driven_by_display(None, &node_labels), None);

    // Known label ⇒ renders the NAME, never the bare full raw key-hex.
    let named = driven_by_display(Some(full_hex), &node_labels).unwrap();
    assert!(named.contains(label), "should show the node name: {named}");
    assert_ne!(named, full_hex, "must not leak the bare full key-hex");

    // No label for this key ⇒ truncated `keyprefix…` form, still not the full hex.
    let other_hex = "deadbeef9876cafef00d";
    let bare = driven_by_display(Some(other_hex), &node_labels).unwrap();
    assert!(bare.contains('…'), "should be the truncated keyprefix form: {bare}");
    assert_ne!(bare, other_hex, "must not leak the bare full key-hex");
    assert!(bare.len() < other_hex.len(), "truncated, shorter than raw hex: {bare}");
}
```

**Coverage:** all three required assertions — `None` in ⇒ `None` out; labeled key ⇒ contains the node name and is not the bare full hex; unlabeled key ⇒ truncated `keyprefix…` form (contains `…`, shorter than and not equal to the full raw hex). BTreeMap built inline; tag placed immediately above; matches the surrounding plain-`#[test]` style.

**Gates:**
1. `cargo test -p spt --bin spt picker::data` — **PASS** — 3 passed; 0 failed (new test + 2 existing data tests green).
2. `cargo clippy --workspace --all-targets -- -D warnings` — **PASS** — `Finished`, zero warnings.

No product defects uncovered; the helper behaves per spec. No gaps left in scope.</result>
<usage><subagent_tokens>54812</subagent_tokens><tool_uses>4</tool_uses><duration_ms>41360</duration_ms></usage>
</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.


Engineer done — test added, workspace clippy `-D warnings`=0, data tests green. Final gate per memory: rebuild the real `spt` bin (not cargo test).


cargo build -p spt --bin spt 2>&1 | Select-Object -Last 8

Rebuild the real spt binary


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


Real bin builds clean. All gates green: workspace clippy `-D warnings`=0 · 33 picker/data unit tests · traceable EXIT=0 · real `spt` bin builds. Commit the W4 work.


Stage all W4 changes


warning: in the working copy of 'crates/spt/tests/attach_wedge_e2e.rs', LF will be replaced by CRLF the next time Git touches it
M  crates/spt/src/cli.rs
M  crates/spt/src/picker/data.rs
M  crates/spt/src/picker/mod.rs
M  crates/spt/src/picker/model.rs
M  crates/spt/src/picker/view.rs
M  crates/spt/tests/attach_wedge_e2e.rs
M  crates/spt/tests/dummy_harness_e2e.rs
M  traceable-reqs.toml


git commit -m @'
feat(picker): W4 picker UX — open on Pick-existing, auto-attach + h-headless, node-name controlled-by, clean bringup output

Four operator-flagged picker UX fixes (REQ-RUN-PICKER + REQ-PICKER-4 extensions):

1. Bare `spt endpoint run` OPENS on Pick-existing (the common case). The Layer-1
   Kind menu becomes a jump-target: `n` jumps to Create-new, Esc backs to it.
   (model new() entry screen.)
2. Offline "Start now" + "Resume from history" now ATTACH by default (were
   detached / no stdout); `h` is the headless escape (start, no attach) on the
   Confirm/Resume screens. confirm_terminal Start->RunMode::Attach,
   start_headless_outcome(), resume_outcome(headless), mod.rs `h` keybinds,
   LEGEND_CONFIRM/LEGEND_RESUME. Single bringup path preserved (cmd_endpoint_run).
3. The "controlled by" pin renders the node NAME, not raw key-hex: data.rs
   node_label_map() + driven_by_display() route a LOCAL row's driven_by through
   the canonical node_label_display (LABEL (keyprefix...), degrades to keyprefix...).
4. The bringup stderr line is a terse machine token — dropped the Rust `Some(..)`
   Debug leak + the "harness binds its perch on startup" internals. `pid=<n>`
   stays parseable (e2e parsers updated pid=Some(->pid=).

Gates: workspace clippy -D warnings = 0; 33 picker/data unit tests green;
traceable-reqs check EXIT=0; real `spt` bin rebuilds. Live keypress path = HITL
verify (no int gate, REQ-RUN-PICKER precedent). 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 W4 picker UX fixes


[v0.13.0-delivery-control ed1aa04] feat(picker): W4 picker UX — open on Pick-existing, auto-attach + h-headless, node-name controlled-by, clean bringup output
 8 files changed, 196 insertions(+), 43 deletions(-)


Committed @ed1aa04. W4 build complete. Per the established route, doyle gates next — notify doyle the work is ready.


Skill

doyle W4 picker UX READY FOR GATE @ed1aa04 (v0.13.0-delivery-control). 4 fixes, all REQ-RUN-PICKER + REQ-PICKER-4 extensions (no new reqs): (1) bare picker opens on Pick-existing, Kind=jump-target, n→create; (2) offline Start-now + Resume-from-history ATTACH by default, `h`=headless escape (start no-attach) on Confirm/Resume screens, single bringup path preserved; (3) controlled-by pin renders node NAME via node_label_display not raw hex (data.rs node_label_map + driven_by_display, LOCAL rows); (4) terse bringup stderr line — dropped Rust Some(..) Debug + "binds its perch" prose, pid=<n> stays parseable (e2e parsers updated). Gates I ran: workspace clippy -D warnings=0; 33 picker/data unit tests green; traceable EXIT=0; real spt bin rebuilds. Live keypress path = HITL verify only (no int gate, REQ-RUN-PICKER precedent — same as the picker decision/render seams). Watch-outs honored: no second bringup path, status-square tri-state untouched, non-interactive REQ-HOST-RUN-1 path unchanged. Your gate next.


Launching skill: spt:send


Base directory for this skill: C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\skills\send

# /spt:send

All commands use `$OWL` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Messaging Command Reference

Both commands read the message body from **stdin** (pipe or heredoc).

```bash
# send -- fire-and-forget to a target (use when you have your own perch)
$OWL send <target> <<'EOF'
message body
EOF

# send --reply-to -- respond to whoever messaged you
$OWL send --reply-to <sender> [your-id] <<'EOF'
response body
EOF

# ask -- deliver + create ephemeral reply perch + poll for reply (use when you have NO perch)
$OWL ask <target> [from] <<'EOF'
message body
EOF
```

If auto-detection fails, pass your ID explicitly: `$OWL send <target> <your-id>`, `$OWL send --reply-to <sender> <your-id>`, `$OWL ask <target> <impromptu-id>`.

**When to use which:**
- **`send`**: You already have a listener. Fire-and-forget, no reply perch created.
- **`send --reply-to`**: You received a message and want to respond. `from` is auto-set from the original sender.
- **`ask`**: You have no listener. Creates a temporary perch, delivers, then polls for the reply.

## Pre-checks

- If `<target>` matches this agent's own listener ID, refuse. Self-sends create loops.
- All send/reply-to/ask commands verify the target is alive automatically.

## If you already have a listener

Just send -- no reply perch needed. Run **not** in background:

```bash
$OWL send <target> <<'EOF'
<msg>
EOF
```

## If you have no listener

The `ask` subcommand creates an ephemeral reply perch, delivers, and polls for the reply. Run with `run_in_background: true` (or `false` for `--block`):

```bash
$OWL ask <target> <<'EOF'
<msg>
EOF
```

## On response

**Important:** If you have an active listener perch, always launch Agent tool calls with `run_in_background: true`. Foreground agents block your poll loop -- no messages (including replies to your send) can be delivered until the agent finishes.

When the response arrives, present it to the user. Responses arrive via one of three paths:
- **Background interject** -- the `$OWL ask` background task completes with the reply.
- **Inline** -- if you used `--block`, the reply prints directly.
- **Hook-injected XML** -- if you are mid-tool-call, the reply appears as `<owl_messages>` XML in your tool call context. The `from` attribute is the sender.

ARGUMENTS: doyle W4 picker UX READY FOR GATE @ed1aa04 (v0.13.0-delivery-control). 4 fixes, all REQ-RUN-PICKER + REQ-PICKER-4 extensions (no new reqs): (1) bare picker opens on Pick-existing, Kind=jump-target, n→create; (2) offline Start-now + Resume-from-history ATTACH by default, `h`=headless escape (start no-attach) on Confirm/Resume screens, single bringup path preserved; (3) controlled-by pin renders node NAME via node_label_display not raw hex (data.rs node_label_map + driven_by_display, LOCAL rows); (4) terse bringup stderr line — dropped Rust Some(..) Debug + "binds its perch" prose, pid=<n> stays parseable (e2e parsers updated). Gates I ran: workspace clippy -D warnings=0; 33 picker/data unit tests green; traceable EXIT=0; real spt bin rebuilds. Live keypress path = HITL verify only (no int gate, REQ-RUN-PICKER precedent — same as the picker decision/render seams). Watch-outs honored: no second bringup path, status-square tri-state untouched, non-interactive REQ-HOST-RUN-1 path unchanged. Your gate next.


I have a perch — fire-and-forget `send`:


Bash

$OWL send doyle <<'EOF'
W4 picker UX READY FOR GATE @ed1aa04 (v0.13.0-delivery-control). 4 fixes, all REQ-RUN-PICKER + REQ-PICKER-4 extensions (no new reqs):
(1) bare picker OPENS on Pick-existing, Kind=jump-target, n→create;
(2) offline Start-now + Resume-from-history ATTACH by default, `h`=headless escape (start no-attach) on Confirm/Resume screens, single bringup path preserved;
(3) controlled-by pin renders node NAME via node_label_display not raw hex (data.rs node_label_map + driven_by_display, LOCAL rows);
(4) terse bringup stderr line — dropped Rust Some(..) Debug + "binds its perch" prose, pid=<n> stays parseable (e2e parsers updated pid=Some(→pid=).
Gates I ran: workspace clippy -D warnings=0; 33 picker/data unit tests green; traceable EXIT=0; real spt bin rebuilds.
Live keypress path = HITL verify only (no int gate, REQ-RUN-PICKER precedent — picker decision/render seams unit-tested, live loop manual-verify).
Watch-outs honored: no second bringup path; status-square tri-state untouched; non-interactive REQ-HOST-RUN-1 path unchanged. Your gate next.
EOF

Send W4 gate-request to doyle


➜ SENT:doyle


Sent. **W4 picker UX shipped @ed1aa04, handed to doyle for gate.**

Done:
- 4 fixes (open-on-Pick-existing · auto-attach + `h` headless · node-name controlled-by · clean bringup output)
- 1 spt-test-engineer added the `driven_by_display` unit (item 3 seam)
- Gates: workspace clippy `-D warnings`=0 · 33 picker/data unit tests · traceable EXIT=0 · real `spt` bin rebuilds
- REQ-RUN-PICKER + REQ-PICKER-4 extended with dated notes (no new reqs)

Next: doyle gate. v0.13.0 PR bundles W1/W1b/W2/W3/W5/W4 + gravity-linux green (doyle-tracked). Nothing blocking on my side.


<task-notification>
<task-id>bv8r78fk1</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/5" id="d02ad7ac" type="msg" from="doyle">doyle W4 GATE PASS — @ed1aa04. Verified myself: picker/data units 32 green + non-vacuous (kind_routes flipped to Screen::PickExisting; confirm_terminal Start→Attach + start_headless_outcome None-when-online; driven_by_display name-not-hex + truncation + None); clippy --workspace -D warnings = 0; traceable EXIT=0. Al</EVENT-PART>
<EVENT-PART seq="2/5" id="d02ad7ac">l 4 fixes correct (open-on-Pick-existing/n-jump, attach-by-default + h-headless, node-name controlled-by via the canonical node_label_display, terse pid=-parseable bringup line). Watch-outs honored: single bringup path (cmd_endpoint_run), status-square tri-state untouched, REQ-HOST-RUN-1 non-interactive path unchanged. No int = correct (REQ-RU</EVENT-PART>
<EVENT-PART seq="3/5" id="d02ad7ac">N-PICKER precedent: seams unit-tested, live loop HITL).

TRACEABILITY HYGIENE (I fixed it @8b12205, doc-only): you folded W4 into REQ-RUN-PICKER + REQ-PICKER-4 (right call — these fixes ARE refinements of those reqs, not new scope). That left the W0-minted REQ-PICKER-UX-V013 a dangling orphan SEED. I dispositioned it = a RETIRED/REDIRE</EVENT-PART>
<EVENT-PART seq="4/5" id="d02ad7ac">CT breadcrumb pointing to where the scope landed, so it's not read as unbuilt. FYI for next time: when folding a wave's minted seed into existing reqs, redirect the seed in the same commit.

W4 done (operator HITL pending — folds into the same dogfood session as the W7 deploy). That's ALL v0.13.0 BUILD waves gated: W1·W1b·W2·W3·W4</EVENT-PART>
<EVENT-PART seq="5/5" id="d02ad7ac">·W5·W7·F1. Remaining = W6 (perri, mine, release-gated) + deferred (Layer F gravity, A2 SEED, W1b F2 pending-check) + the gravity-linux CI leg at PR. Milestone is build-complete. Excellent run — repro-first + real-fixture discipline held across the whole cluster.</EVENT-PART></event>
</task-notification>


doyle GATE PASS @ed1aa04. All v0.13.0 BUILD waves gated (W1·W1b·W2·W3·W4·W5·W7·F1) — milestone build-complete. doyle flagged + fixed (@8b12205, doc-only) a traceability-hygiene miss: I folded W4 into existing reqs but left the W0-minted `REQ-PICKER-UX-V013` seed dangling. Lesson worth banking.


Write

---
name: redirect-folded-req-seeds
description: When folding a wave's minted REQ seed into existing reqs, redirect the seed in the SAME commit so it isn't read as unbuilt scope.
metadata:
  type: feedback
---

When a wave's work folds into EXISTING `REQ-*` entries (it's a refinement, not new scope), the originally-minted placeholder/SEED req for that wave becomes a dangling orphan — read as unbuilt scope unless dispositioned.

**Why:** v0.13.0 W4 — I correctly folded the 4 picker fixes into REQ-RUN-PICKER + REQ-PICKER-4 (refinements, no new reqs), but left the W0-minted `REQ-PICKER-UX-V013` seed dangling. doyle caught it + dispositioned it @8b12205 (doc-only) as a RETIRED/REDIRECT breadcrumb pointing to where the scope landed.

**How to apply:** In the SAME commit where you fold a wave into existing reqs, redirect/retire the wave's minted seed in `traceable-reqs.toml` — a breadcrumb noting which reqs absorbed the scope. Don't leave it for the gate. See [[spt-verification-gates]].


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

---
name: redirect-folded-req-seeds
description: "When folding a wave's minted REQ seed into existing reqs, redirect the seed in the SAME commit so it isn't read as unbuilt scope."
metadata:
  node_type: memory
  type: feedback
  originSessionId: aa228ae4-66f5-4740-9871-bea45aede5d5
---

When a wave's work folds into EXISTING `REQ-*` entries (it's a refinement, not new scope), the originally-minted placeholder/SEED req for that wave becomes a dangling orphan — read as unbuilt scope unless dispositioned.

**Why:** v0.13.0 W4 — I correctly folded the 4 picker fixes into REQ-RUN-PICKER + REQ-PICKER-4 (refinements, no new reqs), but left the W0-minted `REQ-PICKER-UX-V013` seed dangling. doyle caught it + dispositioned it @8b12205 (doc-only) as a RETIRED/REDIRECT breadcrumb pointing to where the scope landed.

**How to apply:** In the SAME commit where you fold a wave into existing reqs, redirect/retire the wave's minted seed in `traceable-reqs.toml` — a breadcrumb noting which reqs absorbed the scope. Don't leave it for the gate. See [[spt-verification-gates]].


1	# Memory index
2
3	- [Cross-machine test rig](cross-machine-test-rig.md) — enlyzeam (Windows, ssh key) for two-host network spikes + Tailscale-down gotcha; gravity-linux (Ubuntu, david/bigscreen pw-auth + askpass shim) as the Linux/forkpty build host.
4	- [CI traceability gate](ci-traceability-gate.md) — how CI installs traceable-reqs (TRACEABLE_REQS_TOKEN PAT + latest BigscreenVR release); the v0.1.0-stale-binary gotcha + the v0.1.1 fix; I have admin to cut releases.
5	- [Attractor build harness](attractor-build-harness.md) — attractor+pi/codex setup at ../attractor; codex model = openai-codex/gpt-5.5 (NOT the SPEC's gpt-5.3-codex); viable for bounded single-machine spt-core tasks (SPIKE-07b gix pilot).
6	- [Windows UAC binary-name 740](windows-uac-binary-name-740.md) — exe names containing update/setup/install/patch trigger UAC elevation (os error 740); name self-update artifacts + test bins to dodge it.
7	- [M4 resume state](m4-resume-state.md) — M4 (spt-net) in progress 2026-06-03; D0+D1 done (iroh =0.98.2 endpoint, identity-bound, relay policy, mDNS); spt-net = first async crate; next = D2 pairing.
8	- [Linux runner migration](linux-runner-migration.md) — replace gravity CI Linux runner with kitsubito (LAN Linux), not WSL (disk cost); deferred until after v0.1.1; retarget `gravity` host-label in ci/release/docs-publish workflows to a role label.
9	- [Delegate tests to spt-test-engineer](delegate-tests-to-spt-test-engineer.md) — write spt-core tests via the `spt-test-engineer` subagent (Agent tool subagent_type), not inline.
10	- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.
11

# Memory index

- [Cross-machine test rig](cross-machine-test-rig.md) — enlyzeam (Windows, ssh key) for two-host network spikes + Tailscale-down gotcha; gravity-linux (Ubuntu, david/bigscreen pw-auth + askpass shim) as the Linux/forkpty build host.
- [CI traceability gate](ci-traceability-gate.md) — how CI installs traceable-reqs (TRACEABLE_REQS_TOKEN PAT + latest BigscreenVR release); the v0.1.0-stale-binary gotcha + the v0.1.1 fix; I have admin to cut releases.
- [Attractor build harness](attractor-build-harness.md) — attractor+pi/codex setup at ../attractor; codex model = openai-codex/gpt-5.5 (NOT the SPEC's gpt-5.3-codex); viable for bounded single-machine spt-core tasks (SPIKE-07b gix pilot).
- [Windows UAC binary-name 740](windows-uac-binary-name-740.md) — exe names containing update/setup/install/patch trigger UAC elevation (os error 740); name self-update artifacts + test bins to dodge it.
- [M4 resume state](m4-resume-state.md) — M4 (spt-net) in progress 2026-06-03; D0+D1 done (iroh =0.98.2 endpoint, identity-bound, relay policy, mDNS); spt-net = first async crate; next = D2 pairing.
- [Linux runner migration](linux-runner-migration.md) — replace gravity CI Linux runner with kitsubito (LAN Linux), not WSL (disk cost); deferred until after v0.1.1; retarget `gravity` host-label in ci/release/docs-publish workflows to a role label.
- [Delegate tests to spt-test-engineer](delegate-tests-to-spt-test-engineer.md) — write spt-core tests via the `spt-test-engineer` subagent (Agent tool subagent_type), not inline.
- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.


Edit

- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.

- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.
- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).


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

- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.
- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).

# Memory index

- [Cross-machine test rig](cross-machine-test-rig.md) — enlyzeam (Windows, ssh key) for two-host network spikes + Tailscale-down gotcha; gravity-linux (Ubuntu, david/bigscreen pw-auth + askpass shim) as the Linux/forkpty build host.
- [CI traceability gate](ci-traceability-gate.md) — how CI installs traceable-reqs (TRACEABLE_REQS_TOKEN PAT + latest BigscreenVR release); the v0.1.0-stale-binary gotcha + the v0.1.1 fix; I have admin to cut releases.
- [Attractor build harness](attractor-build-harness.md) — attractor+pi/codex setup at ../attractor; codex model = openai-codex/gpt-5.5 (NOT the SPEC's gpt-5.3-codex); viable for bounded single-machine spt-core tasks (SPIKE-07b gix pilot).
- [Windows UAC binary-name 740](windows-uac-binary-name-740.md) — exe names containing update/setup/install/patch trigger UAC elevation (os error 740); name self-update artifacts + test bins to dodge it.
- [M4 resume state](m4-resume-state.md) — M4 (spt-net) in progress 2026-06-03; D0+D1 done (iroh =0.98.2 endpoint, identity-bound, relay policy, mDNS); spt-net = first async crate; next = D2 pairing.
- [Linux runner migration](linux-runner-migration.md) — replace gravity CI Linux runner with kitsubito (LAN Linux), not WSL (disk cost); deferred until after v0.1.1; retarget `gravity` host-label in ci/release/docs-publish workflows to a role label.
- [Delegate tests to spt-test-engineer](delegate-tests-to-spt-test-engineer.md) — write spt-core tests via the `spt-test-engineer` subagent (Agent tool subagent_type), not inline.
- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.

+- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).


**W4 GATE PASS @ed1aa04 — milestone build-complete.**

All v0.13.0 BUILD waves gated: W1·W1b·W2·W3·W4·W5·W7·F1. doyle independently re-verified (32 units non-vacuous, clippy 0, traceable 0); all 4 fixes + watch-outs confirmed correct.

Banked the lesson: redirect a wave's minted REQ seed in the same fold-commit (doyle fixed the orphan `REQ-PICKER-UX-V013` @8b12205).

Remaining (not mine / not now):
- **W6** — perri translation binary (doyle, release-gated)
- **Deferred** — Layer F gravity, A2 SEED, W1b F2 pending-check
- **PR** — gravity-linux CI leg (doyle-tracked) + operator HITL of the live picker (folds into W7 deploy dogfood)

Nothing blocking on my side.


<task-notification>
<task-id>bv8r78fk1</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/10" id="c0bcbcf4" type="msg" from="doyle">doyle /diagnose RESULT + DISPATCH — the &quot;daemon window respawns after stop&quot; bug. ROOT-CAUSED + deterministic repro. REOPEN REQ-HAZARD-WMI-DAEMON-WINDOW (the @15fdf58 fix is INSUFFICIENT).

REPRO (deterministic, 3/3): `spt daemon stop` then any spt command (ensure_running) → launch_daemon_job_neutral </EVENT-PART>
<EVENT-PART seq="2/10" id="c0bcbcf4">→ rung = WMI on this box (DAEMON_LAUNCH_VIA_WMI). The WMI-launched daemon ALWAYS gets a conhost child = a console WINDOW. Loop: stop → marked start (SPT_LAUNCH_RUNG_MARKER) → assert no conhost child of the `daemon run` pid.

ROOT: wrapped_daemon_command = `cmd.exe /c set SPT_*… &amp; start &quot;&quot; /b spt daemon run`. @15fdf5</EVENT-PART>
<EVENT-PART seq="3/10" id="c0bcbcf4">8 set Win32_ProcessStartup CreateFlags=DETACHED_PROCESS on the CMD wrapper (cmd → no console). But `start &quot;&quot; /b spt daemon run` then launches the INNER broker with NO no-window flag and NO console to inherit → the broker (console-subsystem exe) ALLOCATES ITS OWN conhost = the window. Proof: the conhost is a CHILD of the `daemon r</EVENT-PART>
<EVENT-PART seq="4/10" id="c0bcbcf4">un` pid, not cmd. The brain is already safe (brainproc.rs:945 CREATE_NO_WINDOW — its comment literally says &quot;the broker has a console&quot;); the BROKER itself is the unprotected one. (breakaway/injob rungs use create_process_detached+CREATE_NO_WINDOW = no console = no window; only WMI+schtasks — the cmd-wrapper rungs — window.)
</EVENT-PART>
<EVENT-PART seq="5/10" id="c0bcbcf4">
FIX (broker self-detach, launch-agnostic): cmd_daemon_run (cli.rs:1865) calls FreeConsole() on Windows IFF launched detached — NOT unconditionally (it's ALSO the foreground-debug `spt daemon run` path; FreeConsole there would kill the dev's terminal view). Signal: add a HIDDEN `--detached` flag to the daemon-run args that the LADDER appe</EVENT-PART>
<EVENT-PART seq="6/10" id="c0bcbcf4">nds on every rung (launch_daemon_job_neutral / brain_child_args sibling), absent when a human types `spt daemon run`. cmd_daemon_run: if --detached set → FreeConsole() (ignore the err on rungs that already have no console — breakaway/injob). This kills the conhost for WMI+schtasks + is a no-op for the already-windowless rungs + preserves f</EVENT-PART>
<EVENT-PART seq="7/10" id="c0bcbcf4">oreground debug. (Alt considered + rejected: fixing the cmd wrapper — cmd/start can't pass CREATE_NO_WINDOW to the inner exe; PS Start-Process -WindowStyle Hidden still leaves a conhost. Broker FreeConsole is the robust fix.)

REGRESSION SEAM: job_escape_e2e.rs already cold-starts via WMI (with the runner-job SKIP guards) — extend it</EVENT-PART>
<EVENT-PART seq="8/10" id="c0bcbcf4"> (or a sibling) to assert the WMI-cold-started `daemon run` pid has NO conhost child (the window check). Real-WMI integration assertion. + a unit on the pure seam (daemon_run_detaches_console_when_--detached / arg-parse set
...(truncated)</event>
</task-notification>


doyle dispatched a new bug to me (root-caused + deterministic repro): **REOPEN REQ-HAZARD-WMI-DAEMON-WINDOW** — daemon window respawns after stop; @15fdf58 fix insufficient. The message truncated at seq 8/10. Let me get the tail from doyle AND ground the code in parallel.


Bash

$OWL send doyle <<'EOF'
ACK the WMI-daemon-window dispatch — root cause + FreeConsole(--detached) fix design clear (broker self-detach, ladder appends hidden --detached, cmd_daemon_run FreeConsole iff set, no-op on already-windowless rungs, preserves foreground debug). Monitor truncated your msg at seq 8/10 — resend seq 9-10 (the test-seam tail + REQ posture: which stages to reactivate on REQ-HAZARD-WMI-DAEMON-WINDOW, and confirm it's mine to build / yours to gate). Grounding the code now (cli.rs cmd_daemon_run + ladder rungs + job_escape_e2e), repro-first.
EOF

Ask doyle for truncated tail seq 9-10


crates\spt-daemon\src\brainproc.rs:935:    cmd.args(brain_child_args(generation, reason));
crates\spt-daemon\src\brainproc.rs:973:fn brain_child_args(generation: u64, reason: StartReason) -> Vec<String> {
crates\spt-daemon\src\brainproc.rs:1183:    fn brain_child_args_carry_generation_and_reason() {
crates\spt-daemon\src\brainproc.rs:1185:            brain_child_args(7, StartReason::Update),
crates\spt-daemon\src\brainproc.rs:1189:            brain_child_args(0, StartReason::Cold),
crates\spt-daemon\src\daemon.rs:613:        launch_daemon_job_neutral(&exe, &args)?;
crates\spt-daemon\src\daemon.rs:660:/// fallback rung and, for the daemon, the [`launch_daemon_job_neutral`] ladder.
crates\spt-daemon\src\daemon.rs:669:/// launcher ladder ([`launch_daemon_job_neutral`]).
crates\spt-daemon\src\daemon.rs:775:/// job-neutral once the daemon itself is (see [`launch_daemon_job_neutral`]), so
crates\spt-daemon\src\daemon.rs:863:pub(crate) fn launch_daemon_job_neutral(program: &str, args: &[String]) -> io::Result<u32> {
crates\spt-daemon\src\daemon.rs:888:    if let Ok(marker) = std::env::var("SPT_LAUNCH_RUNG_MARKER") {
crates\spt-daemon\src\daemon.rs:914:fn wrapped_daemon_command(program: &str, args: &[String]) -> String {
crates\spt-daemon\src\daemon.rs:973:    let wmi_cmdline = wrapped_daemon_command(program, args);
crates\spt-daemon\src\daemon.rs:1029:    let cmdline = wrapped_daemon_command(program, args);
crates\spt\tests\job_escape_e2e.rs:18://!      the WMI rung and asserting the `DAEMON_LAUNCH_VIA_WMI` marker fired (a
crates\spt\tests\job_escape_e2e.rs:191:            .env("SPT_LAUNCH_RUNG_MARKER", &marker)
crates\spt\src\cli.rs:1865:fn cmd_daemon_run() -> i32 {


Sent. Now ground the fix sites — read `cmd_daemon_run`, the launch ladder, `wrapped_daemon_command`, and `brain_child_args`: