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)

        Screen::Confirm => render_confirm(model, f, area),
    }
}

        Screen::Confirm => render_confirm(model, f, area),
        Screen::Resume => render_resume(model, f, area),
    }
}

Screen::CreateId => render_create_id(model, f, area),

Screen::PickExisting => render_pick(model, f, area),

+        Screen::Resume => render_resume(model, f, area),


Edit

#[cfg(test)]
mod tests {
    use super::*;
    use crate::picker::model::{AdapterInfo, EndpointRow, ResumeRow};

// ── Resume-from-history ─────────────────────────────────────────────────────
fn render_resume(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(1)])
        .split(area);
    let rows = model.resume_rows();
    let items: Vec<ListItem> = if rows.is_empty() {
        vec![ListItem::new("(no prior sessions in the ledger)")]
    } else {
        rows.iter()
            .enumerate()
            .map(|(i, r)| item(r.title(), i == model.resume_cursor))
            .collect()
    };
    f.render_widget(
        List::new(items).block(titled_block("Resume from a prior session:")),
        chunks[0],
    );
    f.render_widget(legend(), chunks[1]);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::picker::model::{AdapterInfo, EndpointRow, ResumeRow};


// ── Resume-from-history ─────────────────────────────────────────────────────
fn render_resume(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(1)])
        .split(area);
    let rows = model.resume_rows();
    let items: Vec<ListItem> = if rows.is_empty() {
        vec![ListItem::new("(no prior sessions in the ledger)")]
    } else {
        rows.iter()
            .enumerate()
            .map(|(i, r)| item(r.title(), i == model.resume_cursor))
            .collect()
    };
    f.render_widget(
        List::new(items).block(titled_block("Resume from a prior session:")),
        chunks[0],
    );
    f.render_widget(legend(), chunks[1]);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::picker::model::{AdapterInfo, EndpointRow, ResumeRow};

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

+fn render_resume(model: &PickerModel, f: &mut Frame, area: Rect) {

+        .direction(Direction::Vertical)

+        .constraints([Constraint::Min(1), Constraint::Length(1)])

+    let items: Vec<ListItem> = if rows.is_empty() {

+        vec![ListItem::new("(no prior sessions in the ledger)")]

+            .map(|(i, r)| item(r.title(), i == model.resume_cursor))

+        List::new(items).block(titled_block("Resume from a prior session:")),

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


        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
        assert!(s.contains("Fork endpoint"));
    }
}

        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
        assert!(s.contains("Fork endpoint"));
    }

    // [unit->REQ-RUN-PICKER] the resume screen lists ledger sessions by title.
    #[test]
    fn resume_buffer_lists_sessions() {
        let mut e = ep("doyle", EpStatus::Offline, true);
        e.resume_rows = vec![ResumeRow {
            session_id: "abcdef12345".into(),
            ts: "2026-06-14T10:00:00Z".into(),
            trigger: "clear".into(),
            project: "spt-core".into(),
        }];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        m.enter_pick();
        m.enter_resume();
        let s = rendered(&m);
        assert!(s.contains("Resume from a prior session"));
        assert!(s.contains("spt-core @ 2026-06-14T10:00:00Z"));
        assert!(s.contains("12345"), "session id5 in the title");
    }
}


        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
        assert!(s.contains("Fork endpoint"));
    }
}

        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
        assert!(s.contains("Fork endpoint"));
    }

    // [unit->REQ-RUN-PICKER] the resume screen lists ledger sessions by title.
    #[test]
    fn resume_buffer_lists_sessions() {
        let mut e = ep("doyle", EpStatus::Offline, true);
        e.resume_rows = vec![ResumeRow {
            session_id: "abcdef12345".into(),
            ts: "2026-06-14T10:00:00Z".into(),
            trigger: "clear".into(),
            project: "spt-core".into(),
        }];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        m.enter_pick();
        m.enter_resume();
        let s = rendered(&m);
        assert!(s.contains("Resume from a prior session"));
        assert!(s.contains("spt-core @ 2026-06-14T10:00:00Z"));
        assert!(s.contains("12345"), "session id5 in the title");
    }
}

+    // [unit->REQ-RUN-PICKER] the resume screen lists ledger sessions by title.

+    fn resume_buffer_lists_sessions() {

+        let mut e = ep("doyle", EpStatus::Offline, true);

+        e.resume_rows = vec![ResumeRow {

+            session_id: "abcdef12345".into(),

+            project: "spt-core".into(),

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

+        m.screen = Screen::PickExisting;

+        assert!(s.contains("Resume from a prior session"));

+        assert!(s.contains("12345"), "session id5 in the title");


Now wire mod.rs (live loop). Add a transient `flash` field to the model for shortcut feedback (lets `s` stay in-loop per spec):


    // Resume sub-screen.
    pub resume_cursor: usize,
}

    // Resume sub-screen.
    pub resume_cursor: usize,

    /// A transient one-line status (e.g. the shortcut-written confirmation),
    /// rendered in the bottom row until the next action clears it.
    pub flash: Option<String>,
}


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

    // Resume sub-screen.
    pub resume_cursor: usize,
}

    // Resume sub-screen.
    pub resume_cursor: usize,

    /// A transient one-line status (e.g. the shortcut-written confirmation),
    /// rendered in the bottom row until the next action clears it.
    pub flash: Option<String>,
}

+    /// A transient one-line status (e.g. the shortcut-written confirmation),

+    /// rendered in the bottom row until the next action clears it.


write the JIT plan for W2, verify it with doyle, execute on their greenlight


            confirm_cursor: 0,
            resume_cursor: 0,
        }
    }

            confirm_cursor: 0,
            resume_cursor: 0,
            flash: None,
        }
    }


            confirm_cursor: 0,
            resume_cursor: 0,
        }
    }

            confirm_cursor: 0,
            resume_cursor: 0,
            flash: None,
        }
    }

filtering: false,
confirm_cursor: 0,
resume_cursor: 0,
+            flash: None,
}
}


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

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() -> 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(),
    }
}

+/// The bottom row: a transient flash message (e.g. shortcut written) when set,

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

+        Some(msg) => Paragraph::new(Line::from(Span::styled(

+            Style::default().fg(Color::Yellow),

fn render_pick(model: &PickerModel, f: &mut Frame, area: Rect) {


    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
    f.render_widget(legend(), 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(legend(), chunks[2]);
}

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

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

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

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

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


Now the live loop + dispatch. First make `cmd_endpoint_run` callable from picker:


// [impl->REQ-HOST-RUN-1]
fn cmd_endpoint_run(
    adapter: &str,
    id: &str,

// [impl->REQ-HOST-RUN-1]
pub(crate) fn cmd_endpoint_run(
    adapter: &str,
    id: &str,


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)

// [impl->REQ-HOST-RUN-1]
fn cmd_endpoint_run(
    adapter: &str,
    id: &str,

// [impl->REQ-HOST-RUN-1]
pub(crate) fn cmd_endpoint_run(
    adapter: &str,
    id: &str,

/// PTY (the harness self-registers its perch on bind), then starts, attaches, or

/// views per the chosen terminal-action flag.


Update M12-W2-PLAN.md with crystallized post-ruling design

Append a "Crystallized design (post-ruling)" section reflecting doyle's M12-W2-RULING.md: Q1 defer, Q2 best-effort, Q3 BUILD resume via sessions::last_k, Q4 sentinel-guarded per-OS shortcut, Q5 additive --create, 2 REQs doc+impl+unit no-int, single-bringup-path invariant.

Add REQ-RUN-PICKER + REQ-RUN-SHORTCUT to traceable-reqs.toml

Add both REQ-* entries (rule 3, before building). Stages doc+impl+unit, NO int (TUI manual-verify leg note per REQ-PAIR-6).

Add ratatui + nucleo-matcher deps; make Run{} adapter/id optional + add --create

spt/Cargo.toml: ratatui 0.29 (pairs crossterm 0.28) + nucleo-matcher. cli.rs Run{}: adapter/id → Option<String>, add --create flag; dispatch bare (no adapter/id) → picker, flags-present → cmd_endpoint_run untouched.

Build picker state model (pure, testable)

spt/src/picker/model.rs: screens (Kind/CreateNew/PickExisting/Confirm), nav transitions, action enum (single source of truth), status→square, adapter→profile tree, ledger→resume titles, filter narrowing, cursor windowing. Heavy unit tests.

Build picker data gather over existing surfaces

spt/src/picker/data.rs: harness adapters+profiles (registered/local_profile_names), endpoints by category project/local/subnet (roster + wansend snapshots + resource_projection), description (adapter:profile, project history best-effort contextstore, endpoint description), sessions::last_k resume rows. No new daemon surface.

Build picker ratatui view (Buffer-testable)

spt/src/picker/view.rs: render fns &Model→Buffer; kind screen, create-new adapter/profile tree + id entry, pick-existing category L/R + grouped list + status squares + two-pane description, confirm layer + options, pinned legend. Glyph set lifted. Buffer asserts via ratatui TestBackend.

Build cc-<id> shortcut generation (REQ-RUN-SHORTCUT)

spt/src/picker/shortcut.rs: per-OS native (.ps1 Win / sh+chmod Unix), bake terminal-action flags (adapter[:profile]+id+create|resume+start|attach|view), sentinel-guarded overwrite (generated-by header marker; overwrite own freely, refuse+warn if no sentinel). Unit-tested content + create-vs-update.

Wire picker live loop + terminal-action dispatch (single bringup path)

spt/src/picker/mod.rs: crossterm raw + ratatui Terminal live loop (manual-verify leg); every terminal action routes through cmd_endpoint_run / cmd_fork / rc pump — NO second bringup path (the gate invariant).

Tag evidence, full suite + clippy -D + traceable check, ping doyle to gate

Tag [impl->]/[unit->] in same change; run full test suite, clippy -D warnings, traceable-reqs check EXIT=0. Then ping doyle to gate (reproduce traceable + read load-bearing tests + confirm single-bringup-path).


Updated task #6 status


Updated task #8 status


Write

//! `spt endpoint run` interactive picker (M12-W2, REQ-RUN-PICKER).
//!
//! Bare `spt endpoint run` (no `--adapter`/`--id`) enters this in-process
//! ratatui picker; the flags-present form stays the REQ-HOST-RUN-1
//! non-interactive path, untouched. The picker is a PURE FRONT-END: every
//! terminal action converges on the [`model::Outcome`] enum, which [`dispatch`]
//! routes through the EXISTING bringup fns ([`crate::cli::cmd_endpoint_run`] /
//! the [`crate::rc`] pump) — there is **no second bringup path** (doyle's
//! binding gate invariant; same discipline as W1.5's one pump).
//!
//! Layering: `model` (pure state + transitions, fully unit-tested), `data`
//! (read-only gather over existing surfaces), `view` (renders a `&PickerModel`
//! to a testable ratatui `Buffer`), `shortcut` (`cc-<id>` generation). This file
//! is the thin live key loop — the manual-verify leg (like REQ-PAIR-6's OS
//! probe); the model/view/shortcut logic under it is unit-asserted.
// [impl->REQ-RUN-PICKER]

mod data;
mod model;
mod shortcut;
mod view;

use std::io::{self, Stdout};

use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;

use model::{ConfirmOption, Outcome, PickerModel, RunMode, Screen};
use shortcut::WriteOutcome;

/// Entry point for `spt endpoint run` when it is NOT fully flagged. Gathers the
/// data, drives the TUI, and dispatches the chosen outcome through the existing
/// bringup fns. Returns the process exit code.
pub fn run(prefill_adapter: Option<String>, prefill_id: Option<String>) -> i32 {
    let project_name = data::project_name();
    let adapter_rows = data::adapter_options();
    let endpoints = data::gather_endpoints();
    let mut model = PickerModel::new(
        project_name,
        adapter_rows,
        endpoints,
        prefill_adapter,
        prefill_id,
    );

    let mut terminal = match setup_terminal() {
        Ok(t) => t,
        Err(e) => {
            eprintln!("PICKER_TTY_FAIL: {e}");
            return 1;
        }
    };
    let outcome = event_loop(&mut terminal, &mut model);
    // Restore the terminal BEFORE dispatching — cmd_endpoint_run / the rc pump
    // own raw mode themselves for the attach.
    restore_terminal(&mut terminal);

    match outcome {
        Ok(outcome) => dispatch(outcome),
        Err(e) => {
            eprintln!("PICKER_FAIL: {e}");
            1
        }
    }
}

type Term = Terminal<CrosstermBackend<Stdout>>;

fn setup_terminal() -> io::Result<Term> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    crossterm::execute!(stdout, EnterAlternateScreen)?;
    Terminal::new(CrosstermBackend::new(stdout))
}

fn restore_terminal(terminal: &mut Term) {
    let _ = disable_raw_mode();
    let _ = crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen);
    let _ = terminal.show_cursor();
}

/// The live key loop. Returns the chosen [`Outcome`]; `Outcome::Shortcut` is
/// handled inline (write the file, flash, stay) so it never escapes the loop.
fn event_loop(terminal: &mut Term, model: &mut PickerModel) -> io::Result<Outcome> {
    loop {
        terminal.draw(|f| view::render(model, f))?;
        let Event::Key(key) = event::read()? else {
            continue;
        };
        // Windows fires Press AND Release; only act on Press.
        if key.kind != KeyEventKind::Press {
            continue;
        }
        // Ctrl-C always cancels.
        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
            return Ok(Outcome::Cancelled);
        }
        // Any key clears a stale flash.
        model.flash = None;

        if let Some(outcome) = handle_key(model, key.code) {
            match outcome {
                Outcome::Shortcut {
                    adapter,
                    id,
                    resume,
                    mode,
                } => {
                    model.flash = Some(write_shortcut_now(&adapter, &id, resume.as_deref(), mode));
                }
                done => return Ok(done),
            }
        }
    }
}

/// Translate one key into a model mutation and/or a yielded outcome. Pure
/// dispatch over the model's transition methods — the model owns the semantics.
fn handle_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {
    // Esc backs out one screen (out of filter mode first); from the kind layer
    // it cancels the whole picker.
    if code == KeyCode::Esc {
        if model.screen == Screen::PickExisting && model.filtering {
            model.end_filter();
            return None;
        }
        return model.back().then_some(Outcome::Cancelled);
    }

    match model.screen {
        Screen::Kind => match code {
            KeyCode::Up => model.kind_cursor = model.kind_cursor.saturating_sub(1),
            KeyCode::Down => model.kind_cursor = (model.kind_cursor + 1).min(1),
            KeyCode::Enter => model.enter_kind(),
            _ => {}
        },
        Screen::CreateAdapter => match code {
            KeyCode::Up => model.move_adapter(-1),
            KeyCode::Down => model.move_adapter(1),
            KeyCode::Enter => model.enter_adapter(),
            _ => {}
        },
        Screen::CreateId => match code {
            KeyCode::Char(c) => model.id_push(c),
            KeyCode::Backspace => model.id_backspace(),
            KeyCode::Enter => return model.create_outcome(),
            _ => {}
        },
        Screen::PickExisting => return handle_pick_key(model, code),
        Screen::Confirm => return handle_confirm_key(model, code),
        Screen::Resume => match code {
            KeyCode::Up => model.move_resume(-1),
            KeyCode::Down => model.move_resume(1),
            KeyCode::Enter => return model.resume_outcome(),
            _ => {}
        },
    }
    None
}

fn handle_pick_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {
    if model.filtering {
        match code {
            KeyCode::Char(c) => model.filter_push(c),
            KeyCode::Backspace => model.filter_backspace(),
            KeyCode::Enter => model.end_filter(),
            KeyCode::Up => model.move_item(-1),
            KeyCode::Down => model.move_item(1),
            _ => {}
        }
        return None;
    }
    match code {
        KeyCode::Left => model.move_category(-1),
        KeyCode::Right => model.move_category(1),
        KeyCode::Up => model.move_item(-1),
        KeyCode::Down => model.move_item(1),
        KeyCode::Char('/') => model.start_filter(),
        // 'n' = new endpoint → jump to create-new.
        KeyCode::Char('n') => model.reenter_create(false),
        KeyCode::Enter => model.enter_pick(),
        _ => {}
    }
    None
}

fn handle_confirm_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {
    match code {
        KeyCode::Up => model.move_confirm(-1),
        KeyCode::Down => model.move_confirm(1),
        // The `s` keybind = build/update the cc-<id> shortcut from every
        // pre-start options set (picker spec).
        KeyCode::Char('s') => return model.confirm_terminal(ConfirmOption::Shortcut),
        KeyCode::Enter => {
            let opt = model.selected_confirm()?;
            return match opt {
                // Immediate terminal actions (Attach/Start/View/Shortcut).
                ConfirmOption::Attach
                | ConfirmOption::Start
                | ConfirmOption::View
                | ConfirmOption::Shortcut => model.confirm_terminal(opt),
                // Screen transitions — all converge back on cmd_endpoint_run.
                ConfirmOption::Resume => {
                    model.enter_resume();
                    None
                }
                // Change-adapter / Instantiate keep the id; Fork starts fresh.
                ConfirmOption::ChangeAdapter | ConfirmOption::Instantiate => {
                    model.reenter_create(true);
                    None
                }
                ConfirmOption::Fork => {
                    model.reenter_create(false);
                    None
                }
            };
        }
        _ => {}
    }
    None
}

/// Write the `cc-<id>` shortcut at the project cwd, returning a one-line status
/// for the flash. (The file write is the only side effect; the TUI stays up.)
fn write_shortcut_now(adapter: &str, id: &str, resume: Option<&str>, mode: RunMode) -> String {
    let dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
    match shortcut::write_shortcut(&dir, id, adapter, resume, mode) {
        Ok(WriteOutcome::Created(p)) => format!("✓ shortcut written: {}", p.display()),
        Ok(WriteOutcome::Updated(p)) => format!("✓ shortcut updated: {}", p.display()),
        Ok(WriteOutcome::RefusedForeign(p)) => format!(
            "! refused: {} exists and was not written by spt (not overwritten)",
            p.display()
        ),
        Err(e) => format!("! shortcut write failed: {e}"),
    }
}

/// Route a terminal outcome through the EXISTING bringup fns — the single
/// bringup core (the gate invariant). Returns the process exit code.
// [impl->REQ-RUN-PICKER]
fn dispatch(outcome: Outcome) -> i32 {
    match outcome {
        Outcome::Run {
            adapter,
            id,
            resume,
            mode,
        } => {
            let (start, view) = match mode {
                RunMode::Start => (true, false),
                RunMode::View => (false, true),
                RunMode::Attach => (false, false),
            };
            // attach is the default action (the `_attach` param is accepted for
            // flag symmetry; start/view are what branch).
            crate::cli::cmd_endpoint_run(&adapter, &id, resume, start, false, view)
        }
        // Attach to an already-live endpoint via the rc pump (not a re-bringup).
        Outcome::Attach { id, view } => match crate::rc::run_attach(&id, view) {
            Ok(()) => 0,
            Err(e) => {
                eprintln!("RC_FAIL:{id}: {e}");
                1
            }
        },
        // Shortcut is handled inline in the loop; reaching here means nothing to do.
        Outcome::Shortcut { .. } | Outcome::Cancelled => 0,
    }
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\mod.rs (file state is current in your context — no need to Read it back)

//! `spt endpoint run` interactive picker (M12-W2, REQ-RUN-PICKER).
//!
//! Bare `spt endpoint run` (no `--adapter`/`--id`) enters this in-process
//! ratatui picker; the flags-present form stays the REQ-HOST-RUN-1
//! non-interactive path, untouched. The picker is a PURE FRONT-END: every
//! terminal action converges on the [`model::Outcome`] enum, which [`dispatch`]
//! routes through the EXISTING bringup fns ([`crate::cli::cmd_endpoint_run`] /
//! the [`crate::rc`] pump) — there is **no second bringup path** (doyle's
//! binding gate invariant; same discipline as W1.5's one pump).
//!
//! Layering: `model` (pure state + transitions, fully unit-tested), `data`
//! (read-only gather over existing surfaces), `view` (renders a `&PickerModel`
//! to a testable ratatui `Buffer`), `shortcut` (`cc-<id>` generation). This file
//! is the thin live key loop — the manual-verify leg (like REQ-PAIR-6's OS
//! probe); the model/view/shortcut logic under it is unit-asserted.
// [impl->REQ-RUN-PICKER]

mod data;
mod model;
mod shortcut;
mod view;

use std::io::{self, Stdout};

use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;

use model::{ConfirmOption, Outcome, PickerModel, RunMode, Screen};
use shortcut::WriteOutcome;

/// Entry point for `spt endpoint run` when it is NOT fully flagged. Gathers the
/// data, drives the TUI, and dispatches the chosen outcome through the existing
/// bringup fns. Returns the process exit code.
pub fn run(prefill_adapter: Option<String>, prefill_id: Option<String>) -> i32 {
    let project_name = data::project_name();
    let adapter_rows = data::adapter_options();
    let endpoints = data::gather_endpoints();
    let mut model = PickerModel::new(
        project_name,
        adapter_rows,
        endpoints,
        prefill_adapter,
        prefill_id,
    );

    let mut terminal = match setup_terminal() {
        Ok(t) => t,
        Err(e) => {
            eprintln!("PICKER_TTY_FAIL: {e}");
            return 1;
        }
    };
    let outcome = event_loop(&mut terminal, &mut model);
    // Restore the terminal BEFORE dispatching — cmd_endpoint_run / the rc pump
    // own raw mode themselves for the attach.
    restore_terminal(&mut terminal);

    match outcome {
        Ok(outcome) => dispatch(outcome),
        Err(e) => {
            eprintln!("PICKER_FAIL: {e}");
            1
        }
    }
}

type Term = Terminal<CrosstermBackend<Stdout>>;

fn setup_terminal() -> io::Result<Term> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    crossterm::execute!(stdout, EnterAlternateScreen)?;
    Terminal::new(CrosstermBackend::new(stdout))
}

fn restore_terminal(terminal: &mut Term) {
    let _ = disable_raw_mode();
    let _ = crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen);
    let _ = terminal.show_cursor();
}

/// The live key loop. Returns the chosen [`Outcome`]; `Outcome::Shortcut` is
/// handled inline (write the file, flash, stay) so it never escapes the loop.
fn event_loop(terminal: &mut Term, model: &mut PickerModel) -> io::Result<Outcome> {
    loop {
        terminal.draw(|f| view::render(model, f))?;
        let Event::Key(key) = event::read()? else {
            continue;
        };
        // Windows fires Press AND Release; only act on Press.
        if key.kind != KeyEventKind::Press {
            continue;
        }
        // Ctrl-C always cancels.
        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
            return Ok(Outcome::Cancelled);
        }
        // Any key clears a stale flash.
        model.flash = None;

        if let Some(outcome) = handle_key(model, key.code) {
            match outcome {
                Outcome::Shortcut {
                    adapter,
                    id,
                    resume,
                    mode,
                } => {
                    model.flash = Some(write_shortcut_now(&adapter, &id, resume.as_deref(), mode));
                }
                done => return Ok(done),
            }
        }
    }
}

/// Translate one key into a model mutation and/or a yielded outcome. Pure
/// dispatch over the model's transition methods — the model owns the semantics.
fn handle_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {
    // Esc backs out one screen (out of filter mode first); from the kind layer
    // it cancels the whole picker.
    if code == KeyCode::Esc {
        if model.screen == Screen::PickExisting && model.filtering {
            model.end_filter();
            return None;
        }
        return model.back().then_some(Outcome::Cancelled);
    }

    match model.screen {
        Screen::Kind => match code {
            KeyCode::Up => model.kind_cursor = model.kind_cursor.saturating_sub(1),
            KeyCode::Down => model.kind_cursor = (model.kind_cursor + 1).min(1),
            KeyCode::Enter => model.enter_kind(),
            _ => {}
        },
        Screen::CreateAdapter => match code {
            KeyCode::Up => model.move_adapter(-1),
            KeyCode::Down => model.move_adapter(1),
            KeyCode::Enter => model.enter_adapter(),
            _ => {}
        },
        Screen::CreateId => match code {
            KeyCode::Char(c) => model.id_push(c),
            KeyCode::Backspace => model.id_backspace(),
            KeyCode::Enter => return model.create_outcome(),
            _ => {}
        },
        Screen::PickExisting => return handle_pick_key(model, code),
        Screen::Confirm => return handle_confirm_key(model, code),
        Screen::Resume => match code {
            KeyCode::Up => model.move_resume(-1),
            KeyCode::Down => model.move_resume(1),
            KeyCode::Enter => return model.resume_outcome(),
            _ => {}
        },
    }
    None
}

fn handle_pick_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {
    if model.filtering {
        match code {
            KeyCode::Char(c) => model.filter_push(c),
            KeyCode::Backspace => model.filter_backspace(),
            KeyCode::Enter => model.end_filter(),
            KeyCode::Up => model.move_item(-1),
            KeyCode::Down => model.move_item(1),
            _ => {}
        }
        return None;
    }
    match code {
        KeyCode::Left => model.move_category(-1),
        KeyCode::Right => model.move_category(1),
        KeyCode::Up => model.move_item(-1),
        KeyCode::Down => model.move_item(1),
        KeyCode::Char('/') => model.start_filter(),
        // 'n' = new endpoint → jump to create-new.
        KeyCode::Char('n') => model.reenter_create(false),
        KeyCode::Enter => model.enter_pick(),
        _ => {}
    }
    None
}

fn handle_confirm_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {
    match code {
        KeyCode::Up => model.move_confirm(-1),
        KeyCode::Down => model.move_confirm(1),
        // The `s` keybind = build/update the cc-<id> shortcut from every
        // pre-start options set (picker spec).
        KeyCode::Char('s') => return model.confirm_terminal(ConfirmOption::Shortcut),
        KeyCode::Enter => {
            let opt = model.selected_confirm()?;
            return match opt {
                // Immediate terminal actions (Attach/Start/View/Shortcut).
                ConfirmOption::Attach
                | ConfirmOption::Start
                | ConfirmOption::View
                | ConfirmOption::Shortcut => model.confirm_terminal(opt),
                // Screen transitions — all converge back on cmd_endpoint_run.
                ConfirmOption::Resume => {
                    model.enter_resume();
                    None
                }
                // Change-adapter / Instantiate keep the id; Fork starts fresh.
                ConfirmOption::ChangeAdapter | ConfirmOption::Instantiate => {
                    model.reenter_create(true);
                    None
                }
                ConfirmOption::Fork => {
                    model.reenter_create(false);
                    None
                }
            };
        }
        _ => {}
    }
    None
}

/// Write the `cc-<id>` shortcut at the project cwd, returning a one-line status
/// for the flash. (The file write is the only side effect; the TUI stays up.)
fn write_shortcut_now(adapter: &str, id: &str, resume: Option<&str>, mode: RunMode) -> String {
    let dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
    match shortcut::write_shortcut(&dir, id, adapter, resume, mode) {
        Ok(WriteOutcome::Created(p)) => format!("✓ shortcut written: {}", p.display()),
        Ok(WriteOutcome::Updated(p)) => format!("✓ shortcut updated: {}", p.display()),
        Ok(WriteOutcome::RefusedForeign(p)) => format!(
            "! refused: {} exists and was not written by spt (not overwritten)",
            p.display()
        ),
        Err(e) => format!("! shortcut write failed: {e}"),
    }
}

/// Route a terminal outcome through the EXISTING bringup fns — the single
/// bringup core (the gate invariant). Returns the process exit code.
// [impl->REQ-RUN-PICKER]
fn dispatch(outcome: Outcome) -> i32 {
    match outcome {
        Outcome::Run {
            adapter,
            id,
            resume,
            mode,
        } => {
            let (start, view) = match mode {
                RunMode::Start => (true, false),
                RunMode::View => (false, true),
                RunMode::Attach => (false, false),
            };
            // attach is the default action (the `_attach` param is accepted for
            // flag symmetry; start/view are what branch).
            crate::cli::cmd_endpoint_run(&adapter, &id, resume, start, false, view)
        }
        // Attach to an already-live endpoint via the rc pump (not a re-bringup).
        Outcome::Attach { id, view } => match crate::rc::run_attach(&id, view) {
            Ok(()) => 0,
            Err(e) => {
                eprintln!("RC_FAIL:{id}: {e}");
                1
            }
        },
        // Shortcut is handled inline in the loop; reaching here means nothing to do.
        Outcome::Shortcut { .. } | Outcome::Cancelled => 0,
    }
}


Module complete. Build the spt crate (first ratatui/nucleo compile — may surface API mismatches).


Bash

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


   Compiling nucleo-matcher v0.3.1
   Compiling derive_more-impl v2.1.1
   Compiling lru v0.12.5
   Compiling hickory-proto v0.26.0-beta.4
   Compiling strum v0.26.3
   Compiling unicode-truncate v1.1.0
   Compiling darling_macro v0.23.0
   Compiling darling v0.23.0
   Compiling derive_more v2.1.1
   Compiling hickory-net v0.26.0-beta.4
   Compiling swarm-discovery v0.6.0-alpha.2
   Compiling n0-future v0.3.2
   Compiling iroh-base v0.98.0
   Compiling noq-proto v0.17.0
   Compiling ratatui v0.29.0
   Compiling n0-watcher v0.6.1
   Compiling netwatch v0.16.0
   Compiling iroh-dns v0.98.0
   Compiling portmapper v0.16.0
   Compiling hickory-resolver v0.26.0-beta.4
   Compiling noq v0.18.0
   Compiling iroh-relay v0.98.0
   Compiling iroh v0.98.2
   Compiling spt-net v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
warning: unused variable: `create`
   --> crates\spt\src\cli.rs:960:17
    |
960 |                 create,
    |                 ^^^^^^ help: try ignoring the field: `create: _`
    |
    = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

error[E0716]: temporary value dropped while borrowed
   --> crates\spt\src\picker\view.rs:132:55
    |
132 |     let p = Paragraph::new(lines).block(titled_block(&format!("New {adapter} endpoint")));
    |                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  - temporary value is freed at the end of this statement
    |                                                       |
    |                                                       creates a temporary value which is freed while still in use
133 |     f.render_widget(p, area);
    |                     - borrow later used here
    |
    = note: consider using a `let` binding to create a longer lived value
    = note: this error originates in the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0716`.
warning: `spt` (bin "spt") generated 1 warning
error: could not compile `spt` (bin "spt") due to 1 previous error; 1 warning emitted


Two fixes — borrow temp in view, unused `create`.


Edit

    let p = Paragraph::new(lines).block(titled_block(&format!("New {adapter} endpoint")));
    f.render_widget(p, area);

    let title = format!("New {adapter} endpoint");
    let p = Paragraph::new(lines).block(titled_block(&title));
    f.render_widget(p, area);


    let p = Paragraph::new(lines).block(titled_block(&format!("New {adapter} endpoint")));
    f.render_widget(p, area);

    let title = format!("New {adapter} endpoint");
    let p = Paragraph::new(lines).block(titled_block(&title));
    f.render_widget(p, area);

-    let p = Paragraph::new(lines).block(titled_block(&format!("New {adapter} endpoint")));

+    let title = format!("New {adapter} endpoint");

+    let p = Paragraph::new(lines).block(titled_block(&title));


            EndpointCmd::Run {
                adapter,
                id,
                create,
                resume,
                start,
                attach,
                view,
            } => match (adapter, id) {

            EndpointCmd::Run {
                adapter,
                id,
                // `--create` is the default-fresh made explicit (REQ-HOST-RUN-1
                // flag symmetry so a shortcut can bake create-vs-resume); at
                // dispatch it only matters as the ABSENCE of --resume, so it is
                // not threaded further. clap already conflicts it with --resume.
                create: _,
                resume,
                start,
                attach,
                view,
            } => match (adapter, id) {


            EndpointCmd::Run {
                adapter,
                id,
                create,
                resume,
                start,
                attach,
                view,
            } => match (adapter, id) {

            EndpointCmd::Run {
                adapter,
                id,
                // `--create` is the default-fresh made explicit (REQ-HOST-RUN-1
                // flag symmetry so a shortcut can bake create-vs-resume); at
                // dispatch it only matters as the ABSENCE of --resume, so it is
                // not threaded further. clap already conflicts it with --resume.
                create: _,
                resume,
                start,
                attach,
                view,
            } => match (adapter, id) {

+                // `--create` is the default-fresh made explicit (REQ-HOST-RUN-1

+                // flag symmetry so a shortcut can bake create-vs-resume); at

+                // dispatch it only matters as the ABSENCE of --resume, so it is

+                // not threaded further. clap already conflicts it with --resume.


Bash

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


   Compiling spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
warning: function `nonempty_categories` is never used
   --> crates\spt\src\picker\data.rs:186:8
    |
186 | pub fn nonempty_categories(endpoints: &[EndpointRow], project_name: &str) -> Vec<Category> {
    |        ^^^^^^^^^^^^^^^^^^^
    |
    = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

warning: method `selected_resume` is never used
   --> crates\spt\src\picker\model.rs:615:12
    |
299 | impl PickerModel {
    | ---------------- method in this implementation
...
615 |     pub fn selected_resume(&self) -> Option<&ResumeRow> {
    |            ^^^^^^^^^^^^^^^

warning: `spt` (bin "spt") generated 2 warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.25s


Compiles. Remove two dead-code items (clippy -D would fail).


Edit

/// The categories that actually have endpoints (for the "(no endpoints — press
/// n to create)" empty-state hint per category).
pub fn nonempty_categories(endpoints: &[EndpointRow], project_name: &str) -> Vec<Category> {
    Category::ALL
        .into_iter()
        .filter(|c| {
            endpoints.iter().any(|e| match c {
                Category::Local => e.is_local,
                Category::Subnet => !e.is_local,
                Category::Project => {
                    e.is_local && e.project_history.iter().any(|p| p == project_name)
                }
            })
        })
        .collect()
}


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

//! Picker **data gather** — read-only projections over the EXISTING surfaces
//! the rest of the CLI already uses. No new daemon surface, no new store
//! (doyle ruling): adapters from the registry, endpoints from the roster + the
//! WAN registry snapshots, descriptions from `info.json`, project history
//! best-effort from the context store, resume rows from the session ledger.
//!
//! Everything here degrades to empty rather than failing — the picker is a
//! front-end; a missing surface means "nothing to show", never a crash.
// [impl->REQ-RUN-PICKER]

use std::path::Path;

use spt_store::perch::{self, ParentHint};
use spt_store::{info, project, sessions};

use super::model::{AdapterInfo, AdapterOption, Category, EndpointRow, EpStatus, ResumeRow};

/// The cwd project name — the Project category label + the project-history match
/// key. Derived exactly like the daemon derives it (remote-url slug → toplevel
/// folder → dir name).
pub fn project_name() -> String {
    let dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
    project::project_id_for_dir(&dir)
}

/// The selectable adapter tree: every active `kind="harness"` adapter with its
/// shipped + local profiles nested under it.
pub fn adapter_options() -> Vec<AdapterOption> {
    let adapters_dir = perch::adapters_dir();
    let mut infos: Vec<AdapterInfo> = Vec::new();
    for (record, manifest) in spt_runtime::registry::registered(&adapters_dir) {
        if manifest.adapter.kind != spt_runtime::manifest::AdapterKind::Harness {
            continue;
        }
        let mut shipped: Vec<String> = manifest.profiles.keys().cloned().collect();
        shipped.sort();
        let local = spt_runtime::registry::local_profile_names(&adapters_dir, &record.name);
        infos.push(AdapterInfo {
            name: record.name,
            shipped,
            local,
        });
    }
    infos.sort_by(|a, b| a.name.cmp(&b.name));
    super::model::build_adapter_tree(&infos)
}

/// Every discoverable endpoint, across all three categories. The model
/// partitions by category; this just produces the union (local perches +
/// subnet registry rows).
// [impl->REQ-RUN-PICKER]
pub fn gather_endpoints() -> Vec<EndpointRow> {
    let mut rows = Vec::new();
    rows.extend(local_rows());
    rows.extend(subnet_rows());
    rows
}

/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
fn local_rows() -> Vec<EndpointRow> {
    crate::roster::enumerate()
        .into_iter()
        .map(|p| {
            let perch_path = perch::resolve_perch_path(&p.id, ParentHint::Infer);
            let rec = info::read_info(&perch_path);
            let status = if p.alive {
                EpStatus::Online
            } else {
                EpStatus::Offline
            };
            let adapter_profile = rec
                .as_ref()
                .and_then(|r| r.adapter.clone())
                .unwrap_or_else(|| "(unknown adapter)".to_string());
            let description = rec
                .as_ref()
                .and_then(|r| r.resources.clone())
                .unwrap_or_default();
            let project_history = project_history_for(&p.id);
            // Resume-from-history is offline + LOCAL only (doyle Q3): only an
            // offline local endpoint reads its ledger.
            let resume_rows = if status == EpStatus::Offline {
                resume_rows_for(&perch_path, &project_history)
            } else {
                Vec::new()
            };
            EndpointRow {
                id: p.id,
                group: "local".to_string(),
                node: "LOCAL".to_string(),
                status,
                is_local: true,
                adapter_profile,
                project_history,
                description,
                resume_rows,
            }
        })
        .collect()
}

/// Subnet endpoints, from the WAN registry snapshots, visibility-gated exactly
/// like `spt endpoint list`.
fn subnet_rows() -> Vec<EndpointRow> {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    let subnets = spt_store::subnet::SubnetStore::load();
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let mut out = Vec::new();
    for s in &subnets.subnets {
        let Some(reg) = regs.get(&s.name) else {
            continue;
        };
        let hide_new = s.hide_new_endpoints;
        for row in resource_projection(reg, |id| vis.hidden(id, &s.name, hide_new)) {
            let status = match row.status {
                spt_net::net::registry::Status::Active
                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
                // Suspended (cold) + Offline render offline in the W2 duality.
                _ => EpStatus::Offline,
            };
            out.push(EndpointRow {
                id: row.endpoint_id,
                group: format!("{}:{}", s.name, row.node),
                node: row.node,
                status,
                is_local: false,
                adapter_profile: row.resources.clone().unwrap_or_default(),
                project_history: Vec::new(),
                description: row.resources.unwrap_or_default(),
                resume_rows: Vec::new(),
            });
        }
    }
    out
}

/// Best-effort project history for a local endpoint, newest→oldest (doyle Q2):
/// scan the context store's `projects/<project>/<id>/` tree for the projects
/// this endpoint carries context in, ordered by the context file's mtime.
/// Empty if the store has nothing — an informational pane, degrade-to-empty.
fn project_history_for(id: &str) -> Vec<String> {
    let root = perch::tracked_dir();
    let projects_dir = root.join("projects");
    let Ok(entries) = std::fs::read_dir(&projects_dir) else {
        return Vec::new();
    };
    let mut found: Vec<(std::time::SystemTime, String)> = Vec::new();
    for e in entries.flatten() {
        let project_id = e.file_name().to_string_lossy().into_owned();
        let ctx = spt_store::contextstore::project_context_file(&root, &project_id, id);
        let Ok(meta) = std::fs::metadata(&ctx) else {
            continue; // this endpoint has no context in this project
        };
        let mtime = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
        found.push((mtime, project_id));
    }
    found.sort_by(|a, b| b.0.cmp(&a.0)); // newest first
    found.into_iter().map(|(_, p)| p).collect()
}

/// Resume rows from the per-endpoint session ledger (newest→oldest), titled in
/// the model. The ledger row carries no project, so the project field is a
/// best-effort fill from the endpoint's most-recent project history; the title
/// falls back to the boundary trigger when that is empty.
fn resume_rows_for(perch_path: &Path, project_history: &[String]) -> Vec<ResumeRow> {
    let project = project_history.first().cloned().unwrap_or_default();
    let mut rows: Vec<ResumeRow> = sessions::last_k(perch_path, sessions::MAX_LEDGER)
        .into_iter()
        .map(|e| ResumeRow {
            session_id: e.session_id,
            ts: e.ts,
            trigger: e.trigger.token().to_string(),
            project: project.clone(),
        })
        .collect();
    rows.reverse(); // ledger is oldest→newest; show newest first
    rows
}

/// The categories that actually have endpoints (for the "(no endpoints — press
/// n to create)" empty-state hint per category).
pub fn nonempty_categories(endpoints: &[EndpointRow], project_name: &str) -> Vec<Category> {
    Category::ALL
        .into_iter()
        .filter(|c| {
            endpoints.iter().any(|e| match c {
                Category::Local => e.is_local,
                Category::Subnet => !e.is_local,
                Category::Project => {
                    e.is_local && e.project_history.iter().any(|p| p == project_name)
                }
            })
        })
        .collect()
}

-/// The categories that actually have endpoints (for the "(no endpoints — press

-/// n to create)" empty-state hint per category).

-pub fn nonempty_categories(endpoints: &[EndpointRow], project_name: &str) -> Vec<Category> {

-            endpoints.iter().any(|e| match c {

-                Category::Local => e.is_local,

-                Category::Subnet => !e.is_local,

-                    e.is_local && e.project_history.iter().any(|p| p == project_name)


use super::model::{AdapterInfo, AdapterOption, Category, EndpointRow, EpStatus, ResumeRow};

use super::model::{AdapterInfo, AdapterOption, EndpointRow, EpStatus, ResumeRow};


//! Picker **data gather** — read-only projections over the EXISTING surfaces
//! the rest of the CLI already uses. No new daemon surface, no new store
//! (doyle ruling): adapters from the registry, endpoints from the roster + the
//! WAN registry snapshots, descriptions from `info.json`, project history
//! best-effort from the context store, resume rows from the session ledger.
//!
//! Everything here degrades to empty rather than failing — the picker is a
//! front-end; a missing surface means "nothing to show", never a crash.
// [impl->REQ-RUN-PICKER]

use std::path::Path;

use spt_store::perch::{self, ParentHint};
use spt_store::{info, project, sessions};

use super::model::{AdapterInfo, AdapterOption, Category, EndpointRow, EpStatus, ResumeRow};

/// The cwd project name — the Project category label + the project-history match
/// key. Derived exactly like the daemon derives it (remote-url slug → toplevel
/// folder → dir name).
pub fn project_name() -> String {
    let dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
    project::project_id_for_dir(&dir)
}

/// The selectable adapter tree: every active `kind="harness"` adapter with its
/// shipped + local profiles nested under it.
pub fn adapter_options() -> Vec<AdapterOption> {
    let adapters_dir = perch::adapters_dir();
    let mut infos: Vec<AdapterInfo> = Vec::new();
    for (record, manifest) in spt_runtime::registry::registered(&adapters_dir) {
        if manifest.adapter.kind != spt_runtime::manifest::AdapterKind::Harness {
            continue;
        }
        let mut shipped: Vec<String> = manifest.profiles.keys().cloned().collect();
        shipped.sort();
        let local = spt_runtime::registry::local_profile_names(&adapters_dir, &record.name);
        infos.push(AdapterInfo {
            name: record.name,
            shipped,
            local,
        });
    }
    infos.sort_by(|a, b| a.name.cmp(&b.name));
    super::model::build_adapter_tree(&infos)
}

/// Every discoverable endpoint, across all three categories. The model
/// partitions by category; this just produces the union (local perches +
/// subnet registry rows).
// [impl->REQ-RUN-PICKER]
pub fn gather_endpoints() -> Vec<EndpointRow> {
    let mut rows = Vec::new();
    rows.extend(local_rows());
    rows.extend(subnet_rows());
    rows
}

/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
fn local_rows() -> Vec<EndpointRow> {
    crate::roster::enumerate()
        .into_iter()
        .map(|p| {
            let perch_path = perch::resolve_perch_path(&p.id, ParentHint::Infer);
            let rec = info::read_info(&perch_path);
            let status = if p.alive {
                EpStatus::Online
            } else {
                EpStatus::Offline
            };
            let adapter_profile = rec
                .as_ref()
                .and_then(|r| r.adapter.clone())
                .unwrap_or_else(|| "(unknown adapter)".to_string());
            let description = rec
                .as_ref()
                .and_then(|r| r.resources.clone())
                .unwrap_or_default();
            let project_history = project_history_for(&p.id);
            // Resume-from-history is offline + LOCAL only (doyle Q3): only an
            // offline local endpoint reads its ledger.
            let resume_rows = if status == EpStatus::Offline {
                resume_rows_for(&perch_path, &project_history)
            } else {
                Vec::new()
            };
            EndpointRow {
                id: p.id,
                group: "local".to_string(),
                node: "LOCAL".to_string(),
                status,
                is_local: true,
                adapter_profile,
                project_history,
                description,
                resume_rows,
            }
        })
        .collect()
}

/// Subnet endpoints, from the WAN registry snapshots, visibility-gated exactly
/// like `spt endpoint list`.
fn subnet_rows() -> Vec<EndpointRow> {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    let subnets = spt_store::subnet::SubnetStore::load();
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let mut out = Vec::new();
    for s in &subnets.subnets {
        let Some(reg) = regs.get(&s.name) else {
            continue;
        };
        let hide_new = s.hide_new_endpoints;
        for row in resource_projection(reg, |id| vis.hidden(id, &s.name, hide_new)) {
            let status = match row.status {
                spt_net::net::registry::Status::Active
                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
                // Suspended (cold) + Offline render offline in the W2 duality.
                _ => EpStatus::Offline,
            };
            out.push(EndpointRow {
                id: row.endpoint_id,
                group: format!("{}:{}", s.name, row.node),
                node: row.node,
                status,
                is_local: false,
                adapter_profile: row.resources.clone().unwrap_or_default(),
                project_history: Vec::new(),
                description: row.resources.unwrap_or_default(),
                resume_rows: Vec::new(),
            });
        }
    }
    out
}

/// Best-effort project history for a local endpoint, newest→oldest (doyle Q2):
/// scan the context store's `projects/<project>/<id>/` tree for the projects
/// this endpoint carries context in, ordered by the context file's mtime.
/// Empty if the store has nothing — an informational pane, degrade-to-empty.
fn project_history_for(id: &str) -> Vec<String> {
    let root = perch::tracked_dir();
    let projects_dir = root.join("projects");
    let Ok(entries) = std::fs::read_dir(&projects_dir) else {
        return Vec::new();
    };
    let mut found: Vec<(std::time::SystemTime, String)> = Vec::new();
    for e in entries.flatten() {
        let project_id = e.file_name().to_string_lossy().into_owned();
        let ctx = spt_store::contextstore::project_context_file(&root, &project_id, id);
        let Ok(meta) = std::fs::metadata(&ctx) else {
            continue; // this endpoint has no context in this project
        };
        let mtime = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
        found.push((mtime, project_id));
    }
    found.sort_by(|a, b| b.0.cmp(&a.0)); // newest first
    found.into_iter().map(|(_, p)| p).collect()
}

/// Resume rows from the per-endpoint session ledger (newest→oldest), titled in
/// the model. The ledger row carries no project, so the project field is a
/// best-effort fill from the endpoint's most-recent project history; the title
/// falls back to the boundary trigger when that is empty.
fn resume_rows_for(perch_path: &Path, project_history: &[String]) -> Vec<ResumeRow> {
    let project = project_history.first().cloned().unwrap_or_default();
    let mut rows: Vec<ResumeRow> = sessions::last_k(perch_path, sessions::MAX_LEDGER)
        .into_iter()
        .map(|e| ResumeRow {
            session_id: e.session_id,
            ts: e.ts,
            trigger: e.trigger.token().to_string(),
            project: project.clone(),
        })
        .collect();
    rows.reverse(); // ledger is oldest→newest; show newest first
    rows
}

-use super::model::{AdapterInfo, AdapterOption, Category, EndpointRow, EpStatus, ResumeRow};

+use super::model::{AdapterInfo, AdapterOption, EndpointRow, EpStatus, ResumeRow};


Now keep `selected_resume` honest by using it in `resume_outcome` (avoids dead-code, single source):


    pub fn resume_outcome(&self) -> Option<Outcome> {
        let ep = self.selected_endpoint()?;
        let row = ep.resume_rows.get(self.resume_cursor)?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(row.session_id.clone()),
            mode: RunMode::Attach,
        })
    }

    pub fn resume_outcome(&self) -> Option<Outcome> {
        let session_id = self.selected_resume()?.session_id.clone();
        let ep = self.selected_endpoint()?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(session_id),
            mode: RunMode::Attach,
        })
    }


    pub fn resume_outcome(&self) -> Option<Outcome> {
        let ep = self.selected_endpoint()?;
        let row = ep.resume_rows.get(self.resume_cursor)?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(row.session_id.clone()),
            mode: RunMode::Attach,
        })
    }

    pub fn resume_outcome(&self) -> Option<Outcome> {
        let session_id = self.selected_resume()?.session_id.clone();
        let ep = self.selected_endpoint()?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(session_id),
            mode: RunMode::Attach,
        })
    }

/// Resume-from-history outcome: bring the endpoint up against the chosen

/// prior session id (`cmd_endpoint_run --resume`).

+        let session_id = self.selected_resume()?.session_id.clone();

-        let row = ep.resume_rows.get(self.resume_cursor)?;

-            resume: Some(row.session_id.clone()),


Bash

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


<owl_messages>
<owl_message from="doyle" priority="high">TWO W2 UPDATES (operator, 2026-06-14) — M12-W2-RULING.md updated:

1) Q1 attached/Kick — NOT dropped, SCHEDULED. Operator: build the broker surface as a dedicated slice ONCE W2 LANDS, then come back and wire the features into the picker. Call it W2.5, sequenced after W2 completes. Scope:
   - broker attach-presence query: "who is attached to endpoint E's PTY, by which node/surface" — derive from PresenceLog conn connect/disconnect if sound, else a new query op.
   - force-detach/kick wire op — its OWN KNOWN-HAZARDS treatment (PTY-ownership invariant + who-may-kick auth). This is the hazard-zone reason it's its own slice.
   - picker: blue ■ attached tri-state + the "Kick <node> and attach" confirm option (offline/online → online/attached/offline).
   Own JIT plan + own gate (ping me a design-check like W2/W3). Build W2 first, this after.

2) Q4 REVISION — Windows shortcut = .cmd, NOT .ps1. Reason (operator-raised, verified): default PATHEXT EXCLUDES .ps1 → a bare/ext-less command name never resolves a .ps1, and .ps1 won't double-click-run without `powershell -File`. .cmd/.bat ARE in PATHEXT. Use .cmd (cleaner errorlevel). Body just calls `spt endpoint run` w/ baked flags.
   Document the invocation reality in --help + the generated-file header:
   - bare `cc-<id>` (no .\, no ext) works only from cmd.exe in project dir (cwd+PATHEXT search).
   - PowerShell needs `.\cc-<id>` (refuses cwd exec, but honors PATHEXT → finds the .cmd).
   - Unix: name it `cc-<id>` (NO extension) + chmod +x; invoke `./cc-<id>` (`.` not on PATH).
   - truly-bare `cc` everywhere = PATH-installed launcher = /spt:setup's job (SCOPE #1), NOT the project-root shortcut.
   Sentinel-guarded overwrite unchanged.

Rest of W2 ruling stands. Keep building.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


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


doyle/operator updates — must ACK first.

**1) Q1 attached/Kick:** NOT dropped — scheduled as **W2.5** (own JIT plan + gate) AFTER W2 lands. No change to current W2 work.
**2) Q4 revision:** Windows shortcut = **.cmd not .ps1** (PATHEXT excludes .ps1). Applying now.

ACK + apply .cmd revision:


cd "C:\Users\decid\Documents\projects\spt-core" && $OWL send doyle <<'EOF'
Both absorbed:

1) W2.5 (attached-presence + Kick) — scheduled AFTER W2 lands, own slice/JIT/gate. Noted; not touching W2 scope. Will ping you a design-check on W2.5 once W2 is gated+committed. Will leave a forward-pointer in the W2 plan + a DEFERRED note so it's not lost.

2) Q4 .cmd revision — applying now. Windows shortcut = cc-<id>.cmd (PATHEXT has .cmd, not .ps1), body = @echo off + REM sentinel + `spt endpoint run <baked flags> %*`. Unix unchanged (cc-<id> no-ext + chmod +x). Documenting invocation reality (cmd.exe cwd+PATHEXT bare; PS needs .\; Unix ./; truly-bare `cc` = /spt:setup PATH-launcher, not this) in the generated-file header + the run --help. Sentinel-guard unchanged. Fixing the line-1 sentinel test (cmd line 1 = @echo off, sentinel now line 2; has_sentinel scans first 3 lines).

W2 build is otherwise complete + compiling: model+data+view+shortcut+live-loop, ratatui Buffer + state-model unit tests written. Running full sweep after the .cmd fix, then ping you to gate.
EOF
echo "EXIT=$?"


➜ SENT:doyle
EXIT=0


Now apply the .cmd revision to shortcut.rs:


Edit

//! - **Current-OS native** (doyle Q4): a `.ps1` on Windows, a POSIX `sh` (+`chmod
//!   +x`) on Unix — a single portable form can't be both.

//! - **Current-OS native** (doyle Q4, rev. 2026-06-14): a `.cmd` on Windows
//!   (NOT `.ps1` — the default `PATHEXT` excludes `.ps1`, so a bare/ext-less
//!   name never resolves one and `.ps1` won't run without `powershell -File`;
//!   `.cmd`/`.bat` ARE in `PATHEXT`), a POSIX `sh` (+`chmod +x`) on Unix — a
//!   single portable form can't be both.
//!
//! **Invocation reality** (baked into the generated header so the user isn't
//! surprised):
//! - Windows + cmd.exe, in the project dir: bare `cc-<id>` works (cwd +
//!   `PATHEXT` search).
//! - Windows + PowerShell: needs `.\cc-<id>` (PowerShell refuses cwd exec but
//!   honors `PATHEXT` → finds the `.cmd`).
//! - Unix: `./cc-<id>` (`.` is not on `PATH`).
//! - A truly-bare `cc` everywhere = a PATH-installed launcher = `/spt:setup`'s
//!   job, not this project-root shortcut.


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

//! - **Current-OS native** (doyle Q4, rev. 2026-06-14): a `.cmd` on Windows
//!   (NOT `.ps1` — the default `PATHEXT` excludes `.ps1`, so a bare/ext-less
//!   name never resolves one and `.ps1` won't run without `powershell -File`;
//!   `.cmd`/`.bat` ARE in `PATHEXT`), a POSIX `sh` (+`chmod +x`) on Unix — a
//!   single portable form can't be both.
//!
//! **Invocation reality** (baked into the generated header so the user isn't
//! surprised):
//! - Windows + cmd.exe, in the project dir: bare `cc-<id>` works (cwd +
//!   `PATHEXT` search).
//! - Windows + PowerShell: needs `.\cc-<id>` (PowerShell refuses cwd exec but
//!   honors `PATHEXT` → finds the `.cmd`).
//! - Unix: `./cc-<id>` (`.` is not on `PATH`).
//! - A truly-bare `cc` everywhere = a PATH-installed launcher = `/spt:setup`'s
//!   job, not this project-root shortcut.

//! `cc-<id>` launcher shortcut generation (the picker `s` keybind,
//! REQ-RUN-SHORTCUT).
//!
//! From any pre-start options set the picker bakes the current selection's
//! NON-INTERACTIVE `spt endpoint run` flags into a launcher script at the
//! project root, so a user can re-launch this exact endpoint with one command
//! (and a `cc-doyle` muscle-memory shortcut).
//!
//! - **Current-OS native** (doyle Q4): a `.ps1` on Windows, a POSIX `sh` (+`chmod
//!   +x`) on Unix — a single portable form can't be both.
//! - **Sentinel-guarded overwrite:** the generator writes + checks a generated-by
//!   header [`SENTINEL`]. It overwrites its OWN prior output freely ("Update"),
//!   but REFUSES a same-named file that lacks the sentinel — never clobbering a
//!   user file.
//! - **Terminal actions only:** adapter[:profile] + id + (create|resume) +
//!   (start|attach|view). Kick/Instantiate/Change-adapter/Fork are
//!   interactive-only and not bakeable.
// [impl->REQ-RUN-SHORTCUT]

use std::path::{Path, PathBuf};

use super::model::RunMode;

/// The generated-by marker. Present on line 1 of every script we write; its
/// presence (and ONLY its presence) authorizes an overwrite.
pub const SENTINEL: &str = "spt-generated cc-shortcut — safe to regenerate (do not remove this line)";

/// What a write did (or refused to do).
#[derive(Debug, PartialEq, Eq)]
pub enum WriteOutcome {
    /// A new shortcut was written at this path.
    Created(PathBuf),
    /// An existing spt-generated shortcut was overwritten.
    Updated(PathBuf),
    /// A same-named file exists but lacks the sentinel — refused (the path is
    /// returned so the caller can warn precisely).
    RefusedForeign(PathBuf),
}

/// The current-OS shortcut filename for an endpoint id.
pub fn shortcut_filename(id: &str) -> String {
    if cfg!(windows) {
        format!("cc-{id}.ps1")
    } else {
        format!("cc-{id}")
    }
}

/// Render the script body for the current OS. Pure (no I/O) so the content is
/// unit-assertable. The first line is the sentinel comment (`#` is a comment in
/// both PowerShell and POSIX sh).
// [impl->REQ-RUN-SHORTCUT]
pub fn render_script(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let args = baked_args(id, adapter, resume, mode);
    if cfg!(windows) {
        // PowerShell: `&` call operator forwards $args; `$args` passes through any
        // extra runtime args the user appends to the shortcut.
        format!(
            "# {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             spt endpoint run {args} @args\n"
        )
    } else {
        format!(
            "#!/bin/sh\n\
             # {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             exec spt endpoint run {args} \"$@\"\n"
        )
    }
}

/// The non-interactive flag string baked into the shortcut — terminal actions
/// only. `--create` (the default-fresh made explicit) is mutually exclusive with
/// `--resume`; attach is the default action (no flag).
fn baked_args(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let mut parts = vec![
        "--adapter".to_string(),
        shell_quote(adapter),
        "--id".to_string(),
        shell_quote(id),
    ];
    match resume {
        Some(session) => {
            parts.push("--resume".to_string());
            parts.push(shell_quote(session));
        }
        None => parts.push("--create".to_string()),
    }
    match mode {
        RunMode::Start => parts.push("--start".to_string()),
        RunMode::View => parts.push("--view".to_string()),
        RunMode::Attach => {} // the default action — no flag
    }
    parts.join(" ")
}

/// Conservative quoting: endpoint ids / adapter names / session ids are charset-
/// constrained (alphanumeric, `-`, `_`, plus `:` in an adapter:profile), so a
/// bare token is safe; only quote if something unexpected appears.
fn shell_quote(s: &str) -> String {
    let safe = s
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | ':' | '.'));
    if safe && !s.is_empty() {
        s.to_string()
    } else {
        // Single-quote for sh; PowerShell also accepts single quotes as literals.
        format!("'{}'", s.replace('\'', "'\\''"))
    }
}

/// Whether `contents` was written by us (carries the [`SENTINEL`] on any of its
/// first few lines).
pub fn has_sentinel(contents: &str) -> bool {
    contents.lines().take(3).any(|l| l.contains(SENTINEL))
}

/// Write (or refuse to overwrite) the shortcut in `dir`. Sentinel-guarded:
/// a fresh path is created; an existing OURS path is updated; an existing
/// foreign path (no sentinel) is refused untouched.
// [impl->REQ-RUN-SHORTCUT]
pub fn write_shortcut(
    dir: &Path,
    id: &str,
    adapter: &str,
    resume: Option<&str>,
    mode: RunMode,
) -> std::io::Result<WriteOutcome> {
    let path = dir.join(shortcut_filename(id));
    let exists = path.exists();
    if exists {
        let current = std::fs::read_to_string(&path).unwrap_or_default();
        if !has_sentinel(&current) {
            return Ok(WriteOutcome::RefusedForeign(path));
        }
    }
    let body = render_script(id, adapter, resume, mode);
    std::fs::write(&path, &body)?;
    set_executable(&path);
    Ok(if exists {
        WriteOutcome::Updated(path)
    } else {
        WriteOutcome::Created(path)
    })
}

#[cfg(unix)]
fn set_executable(path: &Path) {
    use std::os::unix::fs::PermissionsExt;
    if let Ok(meta) = std::fs::metadata(path) {
        let mut perms = meta.permissions();
        perms.set_mode(perms.mode() | 0o755);
        let _ = std::fs::set_permissions(path, perms);
    }
}

#[cfg(not(unix))]
fn set_executable(_path: &Path) {}

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

    // [unit->REQ-RUN-SHORTCUT] the script bakes the terminal-action flags; a
    // fresh run defaults to --create + attach (no action flag).
    #[test]
    fn bakes_create_attach_by_default() {
        let body = render_script("doyle", "claude-spt:fast", None, RunMode::Attach);
        assert!(body.contains("--adapter claude-spt:fast"));
        assert!(body.contains("--id doyle"));
        assert!(body.contains("--create"));
        assert!(!body.contains("--resume"));
        assert!(!body.contains("--start") && !body.contains("--view"));
        assert!(body.lines().next().unwrap().contains(SENTINEL), "sentinel on line 1");
    }

    // [unit->REQ-RUN-SHORTCUT] resume bakes --resume (not --create); view/start
    // bake their action flag.
    #[test]
    fn bakes_resume_and_action() {
        let body = render_script("d", "ccs", Some("sess123"), RunMode::View);
        assert!(body.contains("--resume sess123"));
        assert!(!body.contains("--create"));
        assert!(body.contains("--view"));

        let start = render_script("d", "ccs", None, RunMode::Start);
        assert!(start.contains("--create") && start.contains("--start"));
    }

    // [unit->REQ-RUN-SHORTCUT] sentinel detection only trusts our own header.
    #[test]
    fn sentinel_detection() {
        assert!(has_sentinel(&render_script("x", "a", None, RunMode::Attach)));
        assert!(!has_sentinel("#!/bin/sh\necho hi\n"), "a user script has no sentinel");
    }

    // [unit->REQ-RUN-SHORTCUT] create → update (our own) → refuse (foreign).
    #[test]
    fn write_create_update_refuse() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path();

        // 1. fresh → Created.
        let out = write_shortcut(p, "doyle", "claude-spt", None, RunMode::Attach).unwrap();
        let path = match out {
            WriteOutcome::Created(path) => path,
            other => panic!("expected Created, got {other:?}"),
        };
        assert!(path.exists());

        // 2. re-generate over our own → Updated (different selection rewrites).
        let out = write_shortcut(p, "doyle", "ccs", Some("s9"), RunMode::Start).unwrap();
        assert!(matches!(out, WriteOutcome::Updated(_)));
        let body = std::fs::read_to_string(&path).unwrap();
        assert!(body.contains("--adapter ccs") && body.contains("--resume s9"));

        // 3. a foreign same-named file → RefusedForeign, untouched.
        std::fs::write(&path, "#!/bin/sh\necho mine\n").unwrap();
        let out = write_shortcut(p, "doyle", "claude-spt", None, RunMode::Attach).unwrap();
        assert!(matches!(out, WriteOutcome::RefusedForeign(_)));
        assert_eq!(
            std::fs::read_to_string(&path).unwrap(),
            "#!/bin/sh\necho mine\n",
            "a foreign file is never clobbered"
        );
    }
}

-//! - **Current-OS native** (doyle Q4): a `.ps1` on Windows, a POSIX `sh` (+`chmod

-//!   +x`) on Unix — a single portable form can't be both.

+//! - **Current-OS native** (doyle Q4, rev. 2026-06-14): a `.cmd` on Windows

+//!   (NOT `.ps1` — the default `PATHEXT` excludes `.ps1`, so a bare/ext-less

+//!   name never resolves one and `.ps1` won't run without `powershell -File`;

+//!   `.cmd`/`.bat` ARE in `PATHEXT`), a POSIX `sh` (+`chmod +x`) on Unix — a

+//!   single portable form can't be both.

+//! **Invocation reality** (baked into the generated header so the user isn't

+//! - Windows + cmd.exe, in the project dir: bare `cc-<id>` works (cwd +

+//! - Windows + PowerShell: needs `.\cc-<id>` (PowerShell refuses cwd exec but

+//!   honors `PATHEXT` → finds the `.cmd`).

+//! - A truly-bare `cc` everywhere = a PATH-installed launcher = `/spt:setup`'s

+//!   job, not this project-root shortcut.


/// The current-OS shortcut filename for an endpoint id.
pub fn shortcut_filename(id: &str) -> String {
    if cfg!(windows) {
        format!("cc-{id}.ps1")
    } else {
        format!("cc-{id}")
    }
}

/// Render the script body for the current OS. Pure (no I/O) so the content is
/// unit-assertable. The first line is the sentinel comment (`#` is a comment in
/// both PowerShell and POSIX sh).
// [impl->REQ-RUN-SHORTCUT]
pub fn render_script(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let args = baked_args(id, adapter, resume, mode);
    if cfg!(windows) {
        // PowerShell: `&` call operator forwards $args; `$args` passes through any
        // extra runtime args the user appends to the shortcut.
        format!(
            "# {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             spt endpoint run {args} @args\n"
        )
    } else {
        format!(
            "#!/bin/sh\n\
             # {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             exec spt endpoint run {args} \"$@\"\n"
        )
    }
}

/// The current-OS shortcut filename for an endpoint id: `cc-<id>.cmd` on
/// Windows (PATHEXT-resolvable), `cc-<id>` (no extension) on Unix.
pub fn shortcut_filename(id: &str) -> String {
    if cfg!(windows) {
        format!("cc-{id}.cmd")
    } else {
        format!("cc-{id}")
    }
}

/// Render the script body for the current OS. Pure (no I/O) so the content is
/// unit-assertable. The [`SENTINEL`] rides a comment line near the top (`REM`
/// in cmd, `#` in sh) — [`has_sentinel`] scans the first few lines for it.
// [impl->REQ-RUN-SHORTCUT]
pub fn render_script(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let args = baked_args(id, adapter, resume, mode);
    if cfg!(windows) {
        // A Windows batch shortcut. `%*` forwards any extra runtime args. Invoke
        // bare `cc-<id>` from cmd.exe in the project dir, or `.\cc-<id>` from
        // PowerShell (both resolve the .cmd via PATHEXT).
        format!(
            "@echo off\r\n\
             REM {SENTINEL}\r\n\
             REM cc-{id}: launch the `{id}` endpoint with the baked selection.\r\n\
             REM   cmd.exe (project dir):  cc-{id}      PowerShell:  .\\cc-{id}\r\n\
             spt endpoint run {args} %*\r\n"
        )
    } else {
        // A POSIX sh shortcut. Invoke `./cc-{id}` (`.` is not on PATH); `\"$@\"`
        // forwards extra runtime args.
        format!(
            "#!/bin/sh\n\
             # {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             #   invoke from the project dir as:  ./cc-{id}\n\
             exec spt endpoint run {args} \"$@\"\n"
        )
    }
}


/// The current-OS shortcut filename for an endpoint id: `cc-<id>.cmd` on
/// Windows (PATHEXT-resolvable), `cc-<id>` (no extension) on Unix.
pub fn shortcut_filename(id: &str) -> String {
    if cfg!(windows) {
        format!("cc-{id}.cmd")
    } else {
        format!("cc-{id}")
    }
}

/// Render the script body for the current OS. Pure (no I/O) so the content is
/// unit-assertable. The [`SENTINEL`] rides a comment line near the top (`REM`
/// in cmd, `#` in sh) — [`has_sentinel`] scans the first few lines for it.
// [impl->REQ-RUN-SHORTCUT]
pub fn render_script(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let args = baked_args(id, adapter, resume, mode);
    if cfg!(windows) {
        // A Windows batch shortcut. `%*` forwards any extra runtime args. Invoke
        // bare `cc-<id>` from cmd.exe in the project dir, or `.\cc-<id>` from
        // PowerShell (both resolve the .cmd via PATHEXT).
        format!(
            "@echo off\r\n\
             REM {SENTINEL}\r\n\
             REM cc-{id}: launch the `{id}` endpoint with the baked selection.\r\n\
             REM   cmd.exe (project dir):  cc-{id}      PowerShell:  .\\cc-{id}\r\n\
             spt endpoint run {args} %*\r\n"
        )
    } else {
        // A POSIX sh shortcut. Invoke `./cc-{id}` (`.` is not on PATH); `\"$@\"`
        // forwards extra runtime args.
        format!(
            "#!/bin/sh\n\
             # {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             #   invoke from the project dir as:  ./cc-{id}\n\
             exec spt endpoint run {args} \"$@\"\n"
        )
    }
}

//! `cc-<id>` launcher shortcut generation (the picker `s` keybind,
//! REQ-RUN-SHORTCUT).
//!
//! From any pre-start options set the picker bakes the current selection's
//! NON-INTERACTIVE `spt endpoint run` flags into a launcher script at the
//! project root, so a user can re-launch this exact endpoint with one command
//! (and a `cc-doyle` muscle-memory shortcut).
//!
//! - **Current-OS native** (doyle Q4, rev. 2026-06-14): a `.cmd` on Windows
//!   (NOT `.ps1` — the default `PATHEXT` excludes `.ps1`, so a bare/ext-less
//!   name never resolves one and `.ps1` won't run without `powershell -File`;
//!   `.cmd`/`.bat` ARE in `PATHEXT`), a POSIX `sh` (+`chmod +x`) on Unix — a
//!   single portable form can't be both.
//!
//! **Invocation reality** (baked into the generated header so the user isn't
//! surprised):
//! - Windows + cmd.exe, in the project dir: bare `cc-<id>` works (cwd +
//!   `PATHEXT` search).
//! - Windows + PowerShell: needs `.\cc-<id>` (PowerShell refuses cwd exec but
//!   honors `PATHEXT` → finds the `.cmd`).
//! - Unix: `./cc-<id>` (`.` is not on `PATH`).
//! - A truly-bare `cc` everywhere = a PATH-installed launcher = `/spt:setup`'s
//!   job, not this project-root shortcut.
//! - **Sentinel-guarded overwrite:** the generator writes + checks a generated-by
//!   header [`SENTINEL`]. It overwrites its OWN prior output freely ("Update"),
//!   but REFUSES a same-named file that lacks the sentinel — never clobbering a
//!   user file.
//! - **Terminal actions only:** adapter[:profile] + id + (create|resume) +
//!   (start|attach|view). Kick/Instantiate/Change-adapter/Fork are
//!   interactive-only and not bakeable.
// [impl->REQ-RUN-SHORTCUT]

use std::path::{Path, PathBuf};

use super::model::RunMode;

/// The generated-by marker. Present on line 1 of every script we write; its
/// presence (and ONLY its presence) authorizes an overwrite.
pub const SENTINEL: &str = "spt-generated cc-shortcut — safe to regenerate (do not remove this line)";

/// What a write did (or refused to do).
#[derive(Debug, PartialEq, Eq)]
pub enum WriteOutcome {
    /// A new shortcut was written at this path.
    Created(PathBuf),
    /// An existing spt-generated shortcut was overwritten.
    Updated(PathBuf),
    /// A same-named file exists but lacks the sentinel — refused (the path is
    /// returned so the caller can warn precisely).
    RefusedForeign(PathBuf),
}

/// The current-OS shortcut filename for an endpoint id.
pub fn shortcut_filename(id: &str) -> String {
    if cfg!(windows) {
        format!("cc-{id}.ps1")
    } else {
        format!("cc-{id}")
    }
}

/// Render the script body for the current OS. Pure (no I/O) so the content is
/// unit-assertable. The first line is the sentinel comment (`#` is a comment in
/// both PowerShell and POSIX sh).
// [impl->REQ-RUN-SHORTCUT]
pub fn render_script(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let args = baked_args(id, adapter, resume, mode);
    if cfg!(windows) {
        // PowerShell: `&` call operator forwards $args; `$args` passes through any
        // extra runtime args the user appends to the shortcut.
        format!(
            "# {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             spt endpoint run {args} @args\n"
        )
    } else {
        format!(
            "#!/bin/sh\n\
             # {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             exec spt endpoint run {args} \"$@\"\n"
        )
    }
}

/// The non-interactive flag string baked into the shortcut — terminal actions
/// only. `--create` (the default-fresh made explicit) is mutually exclusive with
/// `--resume`; attach is the default action (no flag).
fn baked_args(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let mut parts = vec![
        "--adapter".to_string(),
        shell_quote(adapter),
        "--id".to_string(),
        shell_quote(id),
    ];
    match resume {
        Some(session) => {
            parts.push("--resume".to_string());
            parts.push(shell_quote(session));
        }
        None => parts.push("--create".to_string()),
    }
    match mode {
        RunMode::Start => parts.push("--start".to_string()),
        RunMode::View => parts.push("--view".to_string()),
        RunMode::Attach => {} // the default action — no flag
    }
    parts.join(" ")
}

/// Conservative quoting: endpoint ids / adapter names / session ids are charset-
/// constrained (alphanumeric, `-`, `_`, plus `:` in an adapter:profile), so a
/// bare token is safe; only quote if something unexpected appears.
fn shell_quote(s: &str) -> String {
    let safe = s
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | ':' | '.'));
    if safe && !s.is_empty() {
        s.to_string()
    } else {
        // Single-quote for sh; PowerShell also accepts single quotes as literals.
        format!("'{}'", s.replace('\'', "'\\''"))
    }
}

/// Whether `contents` was written by us (carries the [`SENTINEL`] on any of its
/// first few lines).
pub fn has_sentinel(contents: &str) -> bool {
    contents.lines().take(3).any(|l| l.contains(SENTINEL))
}

/// Write (or refuse to overwrite) the shortcut in `dir`. Sentinel-guarded:
/// a fresh path is created; an existing OURS path is updated; an existing
/// foreign path (no sentinel) is refused untouched.
// [impl->REQ-RUN-SHORTCUT]
pub fn write_shortcut(
    dir: &Path,
    id: &str,
    adapter: &str,
    resume: Option<&str>,
    mode: RunMode,
) -> std::io::Result<WriteOutcome> {
    let path = dir.join(shortcut_filename(id));
    let exists = path.exists();
    if exists {
        let current = std::fs::read_to_string(&path).unwrap_or_default();
        if !has_sentinel(&current) {
            return Ok(WriteOutcome::RefusedForeign(path));
        }
    }
    let body = render_script(id, adapter, resume, mode);
    std::fs::write(&path, &body)?;
    set_executable(&path);
    Ok(if exists {
        WriteOutcome::Updated(path)
    } else {
        WriteOutcome::Created(path)
    })
}

#[cfg(unix)]
fn set_executable(path: &Path) {
    use std::os::unix::fs::PermissionsExt;
    if let Ok(meta) = std::fs::metadata(path) {
        let mut perms = meta.permissions();
        perms.set_mode(perms.mode() | 0o755);
        let _ = std::fs::set_permissions(path, perms);
    }
}

#[cfg(not(unix))]
fn set_executable(_path: &Path) {}

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

    // [unit->REQ-RUN-SHORTCUT] the script bakes the terminal-action flags; a
    // fresh run defaults to --create + attach (no action flag).
    #[test]
    fn bakes_create_attach_by_default() {
        let body = render_script("doyle", "claude-spt:fast", None, RunMode::Attach);
        assert!(body.contains("--adapter claude-spt:fast"));
        assert!(body.contains("--id doyle"));
        assert!(body.contains("--create"));
        assert!(!body.contains("--resume"));
        assert!(!body.contains("--start") && !body.contains("--view"));
        assert!(body.lines().next().unwrap().contains(SENTINEL), "sentinel on line 1");
    }

    // [unit->REQ-RUN-SHORTCUT] resume bakes --resume (not --create); view/start
    // bake their action flag.
    #[test]
    fn bakes_resume_and_action() {
        let body = render_script("d", "ccs", Some("sess123"), RunMode::View);
        assert!(body.contains("--resume sess123"));
        assert!(!body.contains("--create"));
        assert!(body.contains("--view"));

        let start = render_script("d", "ccs", None, RunMode::Start);
        assert!(start.contains("--create") && start.contains("--start"));
    }

    // [unit->REQ-RUN-SHORTCUT] sentinel detection only trusts our own header.
    #[test]
    fn sentinel_detection() {
        assert!(has_sentinel(&render_script("x", "a", None, RunMode::Attach)));
        assert!(!has_sentinel("#!/bin/sh\necho hi\n"), "a user script has no sentinel");
    }

    // [unit->REQ-RUN-SHORTCUT] create → update (our own) → refuse (foreign).
    #[test]
    fn write_create_update_refuse() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path();

        // 1. fresh → Created.
        let out = write_shortcut(p, "doyle", "claude-spt", None, RunMode::Attach).unwrap();
        let path = match out {
            WriteOutcome::Created(path) => path,
            other => panic!("expected Created, got {other:?}"),
        };
        assert!(path.exists());

        // 2. re-generate over our own → Updated (different selection rewrites).
        let out = write_shortcut(p, "doyle", "ccs", Some("s9"), RunMode::Start).unwrap();
        assert!(matches!(out, WriteOutcome::Updated(_)));
        let body = std::fs::read_to_string(&path).unwrap();
        assert!(body.contains("--adapter ccs") && body.contains("--resume s9"));

        // 3. a foreign same-named file → RefusedForeign, untouched.
        std::fs::write(&path, "#!/bin/sh\necho mine\n").unwrap();
        let out = write_shortcut(p, "doyle", "claude-spt", None, RunMode::Attach).unwrap();
        assert!(matches!(out, WriteOutcome::RefusedForeign(_)));
        assert_eq!(
            std::fs::read_to_string(&path).unwrap(),
            "#!/bin/sh\necho mine\n",
            "a foreign file is never clobbered"
        );
    }
}

-/// The current-OS shortcut filename for an endpoint id.

+/// The current-OS shortcut filename for an endpoint id: `cc-<id>.cmd` on

+/// Windows (PATHEXT-resolvable), `cc-<id>` (no extension) on Unix.

-/// unit-assertable. The first line is the sentinel comment (`#` is a comment in

+/// unit-assertable. The [`SENTINEL`] rides a comment line near the top (`REM`

+/// in cmd, `#` in sh) — [`has_sentinel`] scans the first few lines for it.

-        // PowerShell: `&` call operator forwards $args; `$args` passes through any

-        // extra runtime args the user appends to the shortcut.

+        // A Windows batch shortcut. `%*` forwards any extra runtime args. Invoke

+        // bare `cc-<id>` from cmd.exe in the project dir, or `.\cc-<id>` from

+        // PowerShell (both resolve the .cmd via PATHEXT).

-             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\

-             spt endpoint run {args} @args\n"

+             REM cc-{id}: launch the `{id}` endpoint with the baked selection.\r\n\

+             REM   cmd.exe (project dir):  cc-{id}      PowerShell:  .\\cc-{id}\r\n\

+             spt endpoint run {args} %*\r\n"

+        // A POSIX sh shortcut. Invoke `./cc-{id}` (`.` is not on PATH); `\"$@\"`

+        // forwards extra runtime args.

+             #   invoke from the project dir as:  ./cc-{id}\n\


Fix the line-1 sentinel test (sentinel is line 2 in .cmd now):


        assert!(!body.contains("--start") && !body.contains("--view"));
        assert!(body.lines().next().unwrap().contains(SENTINEL), "sentinel on line 1");
    }

        assert!(!body.contains("--start") && !body.contains("--view"));
        assert!(has_sentinel(&body), "sentinel rides a comment line near the top");
    }