# M12 Wave 2 — `spt endpoint run` interactive picker (ratatui) — JIT plan

> todlando 2026-06-14, post W1.5 + W3 gate (both PASS, committed 0c687fe / c02aabb). W2 is
> the BIGGEST remaining wave and the user-facing face of M12. **DESIGN-CHECK PENDING with
> doyle before building** — open questions below center on data surfaces the picker needs
> that may not exist yet (attached-state query, project history, per-endpoint session log,
> kick-and-attach op). Build only after his ruling. Full spec: `M12-ENDPOINT-RUN-PICKER.md`;
> wave scope: `M12-PLAN.md` Wave 2.

## Scope (M12-PLAN.md Wave 2 / picker spec)
- **T2.1** Layer 1 (kind: Create new | Pick existing) + create-new branch: choose harness
  adapter (profiles tree-nested under each adapter) → enter id (charset-validated) → start.
- **T2.2** Pick-existing: category L/R (`<project> | Local node | Subnet`), grouped +
  alpha-sorted, status squares (online green ■ / offline gray ▢ / attached blue ■),
  type-to-filter (`/`), pinned keybind legend, **two-pane right-half description**
  (adapter:profile · project history newest→oldest · `endpoint description`).
- **T2.3** Confirm layer + status-dependent options: Attach/Start/View · **Kick <node> and
  attach** (attached-only) · Instantiate-locally (remote) · Change-adapter (offline) · Fork ·
  **Resume-from-history** (offline-only; per-endpoint session log + manifest `resume`; titles
  `<project> @ <ts> (...id5)`).
- **T2.4** `s` keybind = build/update `cc-<id>` shortcut at project root with baked
  non-interactive flags. Requires the flag set to cover every terminal action of the flow.
- **Phone caveat:** don't hard-couple interaction to keybinds (future tap-mode layer can
  reuse the same state model + action enum).

## What EXISTS today (investigation 2026-06-14)
- **Terminal action core (the picker terminates INTO this):** `cli.rs:cmd_endpoint_run`
  (T1.1) — adapter resolve → `harnesshost::launch_harness_brokered_in` → start/attach/view
  via `rc::run_attach`. Picker = an interactive front-end that gathers (adapter, id,
  resume, action) then calls the same path. `Run{}` flags today: `--adapter <a[:profile]>`
  `--id` `--resume <session>` `--start|--attach|--view`. **No `--create` flag** (run mints
  fresh unless `--resume`); no kick/instantiate/fork-from-run flags.
- **Adapter + profile enumeration:** `registry::resolve_option` (split `<adapter>:<profile>`
  → parent lookup → overlay). Profiles live at `adapters/<adapter>/profiles/<profile>.toml`;
  `registry.rs:local_profiles_dir` + read_dir enumerate them (need a tidy "list harness
  adapters + their profiles" helper for L2; building blocks present). `AdapterKind::Harness`
  gates the harness-only list.
- **Endpoint discovery:** `roster::enumerate()` (local perches: state/ready/alive) +
  `roster::detect_self_id`; `wansend::load_snapshots(identity/registry)` per-subnet snapshots
  → `resource_projection(reg, hidden_fn)` = visible+routable rows `(endpoint_id, node, status,
  resources)`; `VisibilityStore` + `SubnetStore`. This is the L2 pick-existing data spine.
- **Status:** registry `Status{Active,Dormant,Offline,Suspended}` → online/offline mapping.
  **"Attached" (blue ■) has no obvious query** (see Q1).
- **Endpoint description:** `spt_store::info::read_info().resources` (the `endpoint
  description` blurb).
- **Deps:** `crossterm = "0.28"` already in `spt/Cargo.toml` (landed W1 for the rc raw-mode
  pump, explicitly noted "the same crate W2's ratatui picker builds on"). **ratatui +
  nucleo-matcher are NEW deps** to add this wave.
- **Reuse for terminal actions:** `cmd_fork` (Fork), `cmd_rest`/wake (resting), rc pump
  (attach/view) all exist as callable CLI fns.

## Investigation TODOs (read before coding — NOT yet done; gated on doyle scope ruling)
1. **`spt-daemon/src/attach.rs` + `presence.rs`**: is there a query "is endpoint E's
   broker PTY currently attached, and by which node/surface?" `serve_attach` serves a
   stream; PresenceLog tracks conn-level connect/disconnect. Determine whether attached-state
   (for the blue ■ + the "Kick <node>" option) is derivable from existing broker state or
   needs a new query op. (Drives Q1.)
2. **Project history surface**: where is a per-endpoint "projects worked, newest→oldest"
   list? Candidates: p-<project> context branches (contextstore), info.json, or the session
   log. The picker needs it for the description pane + confirm "Project history:". (Drives Q2.)
3. **Per-endpoint session log**: does a session-id log with last-message timestamps exist
   for Resume-from-history? `harnesshost::mint_session_id` mints; the adapter manifest
   `resume` declaration is the other half. Find where prior session ids are recorded (if at
   all) and what timestamp is available. (Drives Q3.)
4. **`vercel-labs/skills` `src/prompts/search-multiselect.ts`**: lift the state model
   `{query, cursor, selected:HashSet, locked:Vec}` + glyph set (`◆ ◇ ■ ● ○ ✓ • │ ─ ❯`) +
   windowed-cursor behavior. Map to ratatui `List`+`ListState`. (Look/feel parity.)
5. **`cmd_fork` / wake / change-adapter call shapes**: confirm the confirm-layer options can
   reuse the existing CLI fns (Fork, Instantiate-locally, Change-adapter, Resume) without
   new daemon plumbing beyond Q1/Q3 gaps.

## Open design questions for doyle
**Scope / data gaps (the load-bearing ones):**
1. **Attached-state (blue ■ + "Kick <node> and attach").** If no broker query exists for
   "who is attached to this PTY," this is the heaviest unknown. Options: **(a)** ship W2 with
   online/offline only, defer the blue tri-state + the Kick option to a follow-up once an
   attach-presence query exists; **(b)** build a broker attach-presence query op now (new
   daemon surface + the force-detach/kick wire op). Recommend **(a)** — keep W2 to the
   picker UX over surfaces that exist; the Kick path is a distinct broker capability with its
   own hazard surface (PTY ownership, KH zone) better scoped as its own slice. Confirm.
2. **Project history.** Is there an authoritative per-endpoint project list to read, or does
   W2 derive it from p-<project> context branches? If neither is cheap, propose rendering
   the description pane's history as "best-effort from contextstore, empty if none" and not
   blocking the wave on a new history store. Your call on the source.
3. **Resume-from-history.** Does a per-endpoint session-id log with timestamps exist? If not,
   Resume-from-history can't enumerate prior sessions honestly. Options: **(a)** defer
   Resume-from-history to when the session log exists (offline endpoints still get
   Attach/Start/View/Fork/Change-adapter); **(b)** establish a minimal per-endpoint session
   log now. Recommend **(a)** unless the log already exists.
4. **`cc-<id>` shortcut format (T2.4).** Cross-platform: a `.ps1` on Windows + a POSIX `sh`
   on Unix at project root, each baking the non-interactive `spt endpoint run` flags? Or a
   single portable form? Confirm the shape + the "Update if exists" overwrite semantics.
5. **Non-interactive flag completeness (T2.4 dependency).** The shortcut bakes the full
   selection. Today `Run{}` lacks `--create` (explicit create-vs-resume) and has no
   kick/instantiate/fork verbs. Proposal: add `--create` (explicit, the default-fresh made
   explicit) and let the shortcut cover only the *terminal* actions reachable
   non-interactively (adapter/profile + id + create|resume + start|attach|view); the
   interactive-only branches (Kick, Instantiate, Change-adapter, Fork) stay picker-only.
   Confirm the additive flag set.

**UX / structure (lower-stakes, my defaults unless you object):**
6. Single-binary in-process module `spt/src/picker/` (state model + ratatui render +
   key/action loop), invoked when `spt endpoint run` is called with no `--adapter`/`--id`
   (bare = interactive; flags present = today's non-interactive path, untouched). Keep the
   action enum the single source of truth so a future tap-mode layers on without touching it.
7. nucleo-matcher for `/` type-to-filter; ratatui two-column `Layout` for the description
   pane; keybind legend as a pinned bottom row. Esc=back, ←→=category, ↑↓=item, enter=confirm,
   /=filter, n=new, s=shortcut.

## Tentative tasks (pending doyle ruling — shapes WILL shift on Q1–Q5)
- **T2.0** Add REQs to `traceable-reqs.toml` FIRST (rule 3). Proposed: **REQ-RUN-PICKER**
  (interactive `spt endpoint run` TUI: kind layer, create-new adapter→profile→id, pick-existing
  category/group/status/filter/two-pane, confirm layer + status-dependent options) and
  **REQ-RUN-SHORTCUT** (`cc-<id>` generation + the additive non-interactive flag set).
  Granularity per doyle. Stages doc+impl+unit this wave (TUI int is harness-driven /
  manual — confirm whether `int` applies or stays a manual-verify like REQ-PAIR-6's OS probe).
- **T2.1** Add ratatui + nucleo-matcher deps. `spt/src/picker/` module: state model
  (`{query,cursor,selected,locked}` lifted) + the Layer-1 kind screen + create-new branch
  (harness-adapter list with profiles tree-nested → id-entry with live charset validation) →
  call `cmd_endpoint_run`. Glyph set + windowed cursor for feel parity.
- **T2.2** Pick-existing screen: category L/R over `[<cwd-project> | Local node | Subnet]`,
  grouping + alpha-sort per spec, status squares (scope per Q1), `/` filter (nucleo),
  pinned legend, two-pane description (adapter:profile · project history per Q2 ·
  `endpoint description`).
- **T2.3** Confirm layer + status-dependent option set; wire each option to its existing CLI
  fn (Attach/Start/View via rc; Fork via `cmd_fork`; Change-adapter/Instantiate via
  create-new re-entry; Resume + Kick per Q1/Q3 rulings).
- **T2.4** `s` keybind → write/update the `cc-<id>` shortcut (format per Q4) with baked flags
  (per Q5); add `--create` to `Run{}`.
- Unit: state-model transitions (category nav, filter narrowing, cursor windowing), the
  status→square mapping, the adapter→profile tree build, the shortcut-script content +
  create-vs-update, the flag→action mapping. Activate stages as evidence lands. Full suite +
  clippy -D + `traceable-reqs check` EXIT=0. W2 gate (doyle).

## Crystallized design (2026-06-14, post-ruling — doyle GO, `M12-W2-RULING.md`)
doyle verified surfaces vs source before ruling. Rulings folded in:
- **Q1 attached-state (blue ■ + Kick): SCHEDULED as W2.5** (rev. 2026-06-14, operator: NOT
  dropped — built as a dedicated slice AFTER W2 lands, then wired back into the picker). W2
  ships **online/offline only**. W2.5 scope (own JIT plan + own doyle design-check + own
  gate, sequenced after W2 completes): (1) 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); (2) force-detach/kick wire op with its OWN KNOWN-HAZARDS
  treatment (PTY-ownership invariant + who-may-kick auth — the hazard-zone reason it is its
  own slice); (3) picker: the blue ■ attached tri-state + the "Kick <node> and attach"
  confirm option (status becomes online/attached/offline). `presence.rs` today has
  snapshots/most_recently_active/resolve but no "which node is attached to E's PTY".
- **Q2 project history: best-effort, no new store.** Derive newest→oldest from the existing
  contextstore `p-<project>` branches; render empty if none. Informational pane,
  degrade-to-empty.
- **Q3 Resume-from-history: BUILD IT** (correction — my TODO#3 was un-run; the surface
  EXISTS). `spt-store/src/sessions.rs::last_k(perch, k) -> Vec<SessionEntry{ts RFC3339-UTC,
  session_id, trigger}>` (M10 / ADR-0019 / REQ-TERM-6, bounded MAX_LEDGER=64, garbage-tolerant)
  IS the per-endpoint session log. `--resume <session>` already on `Run{}`. The picker
  enumerates `last_k` for the **offline LOCAL** endpoint, renders `<project> @ <ts> (…id5)`
  (project best-effort; the ledger row carries ts/session_id/trigger, not a project — title
  falls back to the trigger token when no project association), feeds `session_id` →
  `cmd_endpoint_run` with `resume=Some(id)`. NO new daemon surface. Offline+LOCAL only (a
  remote offline endpoint = Instantiate-locally). Empty ledger ⇒ no Resume option. Adapter
  resume-capability is its manifest `resume`'s job, surfaced at the terminal action.
- **Q4 `<basename>-<id>` shortcut: parameterized basename + per-OS native + sentinel-guarded
  overwrite.** **Basename is a PARAMETER** (rev. 2 2026-06-14, operator): harness-agnostic
  spt-core defaults to **`spt`** (→ `spt-<id>`, e.g. `spt-doyle`); an adapter/flow OVERRIDES it
  (spt-claude-code → `cc` → `cc-<id>`) — **spt-core never bakes `cc`** (a harness name) into
  itself. Never bare `spt` (a `spt.cmd` shadows the real `spt.exe` only under cmd.exe cwd-first
  search → inconsistent + self-recursing); a distinct `spt-<id>` is the safe form. Bare "launch
  this project" is already `spt endpoint run` (no-arg picker) — no shadow wrapper.
  **`.cmd` on Windows** (rev. 1 2026-06-14: 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), POSIX `sh` (+`chmod +x`) on Unix. Bake the non-interactive flags. The generated
  header documents the invocation reality (`<name>` = `<basename>-<id>`): 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, SCOPE #1), not this project-root shortcut.
  Overwrite = the generator writes + checks a generated-by **sentinel** header marker:
  overwrite our-own freely, **REFUSE + warn** if a same-named file lacks the sentinel (never
  clobber a user file). "Update if exists" = overwrite-our-own-only.
- **Q5 flags: additive `--create` approved** (N-1-safe; the default-fresh made explicit).
  Shortcut covers **terminal actions only**: adapter[:profile] + id + (create|resume) +
  (start|attach|view). Resume IS bakeable. Kick/Instantiate/Change-adapter/Fork stay
  picker-only (not bakeable).
- **REQs: REQ-RUN-PICKER + REQ-RUN-SHORTCUT** (2-REQ granularity approved). **Stages doc +
  impl + unit; NO `int` on the TUI** — ratatui renders to a testable `Buffer`: assert the
  rendered buffer + state-model transitions (category nav, filter narrowing, cursor
  windowing, status→square, adapter→profile tree, ledger→resume-titles, shortcut content +
  create-vs-update, flag→action) as unit. The live key loop is a **manual-verify leg** (like
  REQ-PAIR-6's OS probe). The integrating bringup path is ALREADY int-covered (W1/W1.5
  `cmd_endpoint_run`); the picker rides it.
- **Q6/Q7 UX defaults: APPROVED.** In-process `spt/src/picker/`; bare `spt endpoint run` (no
  `--adapter`/`--id`) = interactive, flags-present = today's non-interactive path untouched;
  action enum = single source of truth (tap-mode layers on later); nucleo `/` filter;
  ratatui two-column layout; pinned legend.

### Verified source surfaces (build directly on these)
- `registry::registered(adapters_dir) -> Vec<(AdapterRecord, Manifest)>` + `all_records`;
  `manifest.profiles.keys()` = shipped profiles; `registry::local_profile_names` = locals;
  `AdapterKind::Harness` gate. (L2 adapter→profile tree.)
- `roster::enumerate()` / `detect_self_id`; `wansend::load_snapshots(identity/registry)` +
  `resource_projection(reg, hidden_fn)` (`endpoint_id, node, status, resources`);
  `VisibilityStore` + `SubnetStore`. (Pick-existing spine + status.)
- `spt_store::info::read_info(perch).resources` (endpoint description);
  `perch::resolve_perch_path(id, ParentHint::Infer)` → `sessions::last_k(perch, k)` (resume).
- `rc::run_attach(id, view)` (attach/view); `cmd_endpoint_run` (start); `cmd_fork` (Fork).
- crossterm 0.28.1 already in tree (W1 rc raw-mode); ratatui + nucleo-matcher = new deps.

### Binding gate invariant (doyle gates on this)
**No second bringup path** — every terminal action routes through `cmd_endpoint_run` /
existing CLI fns (`cmd_fork`, wake, rc pump). One bringup core, same discipline as W1.5's
one-pump.

## Notes
- The picker is a pure front-end: it MUST NOT introduce a second bringup path — every
  terminal action routes through `cmd_endpoint_run` / the existing CLI fns. One bringup core,
  same invariant discipline as W1.5's one-pump.
- TUI testing: ratatui renders to a `Buffer` (testable without a real terminal) — assert the
  rendered buffer for screen-state unit tests; the live key loop is the manual-verify leg.
- ADR-0001 territory only if any wire/registry format changes (Q1 attach-presence op would
  be) — else clean-room. Additive `Run{}` flags are N-1-safe.
- UNCOMMITTED building until the operator calls a commit (their per-wave pacing).
