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

//! `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: `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 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"
        );
    }
}

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

+        assert!(has_sentinel(&body), "sentinel rides a comment line near the top");


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


Update REQ-RUN-SHORTCUT title (.ps1→.cmd) + plan Q4 + W2.5 forward-pointer:


Edit

The script is the CURRENT OS's native form — `.ps1` on Windows, POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both).

The script is the CURRENT OS's native form — `.cmd` on Windows (NOT `.ps1`: default PATHEXT excludes `.ps1` so a bare/ext-less name never resolves one; `.cmd` is PATHEXT-resolvable — operator rev. 2026-06-14), POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). The generated header documents the invocation reality (cmd.exe bare `cc-<id>` in the project dir / PowerShell `.\cc-<id>` / Unix `./cc-<id>`; a truly-bare `cc` = a PATH-installed launcher, `/spt:setup`'s job).


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

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

-title = "`cc-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `cc-<id>` launcher at the project root baking the current selection's non-interactive `spt endpoint run` flags (terminal actions only: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). The script is the CURRENT OS's native form — `.ps1` on Windows, POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). Overwrite is SENTINEL-guarded: the generator writes + checks a generated-by header marker — it overwrites its own prior output freely (\"Update `cc-<id>` shortcut\"), but REFUSES + warns if a same-named file lacks the sentinel (never clobber a user file). Requires the additive `--create` flag on `Run{}` (the default-fresh made explicit; N-1-safe)."

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

required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: shortcut content + sentinel create-vs-update as unit; flag→action mapping unit)

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


- **Q4 `cc-<id>` shortcut: per-OS native + sentinel-guarded overwrite.** Current-OS only:
  `.ps1` on Windows, POSIX `sh` (+`chmod +x`) on Unix. Bake the non-interactive flags.

- **Q4 `cc-<id>` shortcut: per-OS native + sentinel-guarded overwrite.** Current-OS only:
  **`.cmd` on Windows** (rev. 2026-06-14, operator: 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: cmd.exe bare `cc-<id>` in the
  project dir · PowerShell `.\cc-<id>` · Unix `./cc-<id>` · a truly-bare `cc` = a
  PATH-installed launcher (`/spt:setup`'s job, SCOPE #1), not this project-root shortcut.


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


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

- **Q4 `cc-<id>` shortcut: per-OS native + sentinel-guarded overwrite.** Current-OS only:
  `.ps1` on Windows, POSIX `sh` (+`chmod +x`) on Unix. Bake the non-interactive flags.

- **Q4 `cc-<id>` shortcut: per-OS native + sentinel-guarded overwrite.** Current-OS only:
  **`.cmd` on Windows** (rev. 2026-06-14, operator: 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: cmd.exe bare `cc-<id>` in the
  project dir · PowerShell `.\cc-<id>` · Unix `./cc-<id>` · a truly-bare `cc` = a
  PATH-installed launcher (`/spt:setup`'s job, SCOPE #1), not this project-root shortcut.

remote offline endpoint = Instantiate-locally). Empty ledger ⇒ no Resume option. Adapter

resume-capability is its manifest `resume`'s job, surfaced at the terminal action.

-  `.ps1` on Windows, POSIX `sh` (+`chmod +x`) on Unix. Bake the non-interactive flags.

+  **`.cmd` on Windows** (rev. 2026-06-14, operator: 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: cmd.exe bare `cc-<id>` in the

+  project dir · PowerShell `.\cc-<id>` · Unix `./cc-<id>` · a truly-bare `cc` = 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.


- **Q1 attached-state (blue ■ + Kick): DEFERRED** (option a). W2 ships **online/offline
  only**. The blue tri-state + "Kick <node>" need a new broker attach-presence query AND a
  force-detach wire op — a distinct broker mutation in the PTY-ownership hazard zone → its
  own focused slice + gate, NOT a picker-wave rider. (`presence.rs` has
  snapshots/most_recently_active/resolve but no "which node is attached to E's PTY".)

- **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".


- **Q1 attached-state (blue ■ + Kick): DEFERRED** (option a). W2 ships **online/offline
  only**. The blue tri-state + "Kick <node>" need a new broker attach-presence query AND a
  force-detach wire op — a distinct broker mutation in the PTY-ownership hazard zone → its
  own focused slice + gate, NOT a picker-wave rider. (`presence.rs` has
  snapshots/most_recently_active/resolve but no "which node is attached to E's PTY".)

- **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".

## 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): DEFERRED** (option a). W2 ships **online/offline

-  only**. The blue tri-state + "Kick <node>" need a new broker attach-presence query AND a

-  force-detach wire op — a distinct broker mutation in the PTY-ownership hazard zone → its

-  own focused slice + gate, NOT a picker-wave rider. (`presence.rs` has

-  snapshots/most_recently_active/resolve but no "which node is attached to E's PTY".)

+- **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,


Add DEFERRED.md note for W2.5 (doyle asked). Check format:


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && tail -20 docs/DEFERRED.md


| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |
| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite holding only the elevated children, linked back to the unelevated daemon — rides the REQ-CONSENT-2 escalation framework (allow-once/always) + the reserved-but-refusing capability pattern | Elevated-endpoint design grill (user 2026-06-06) | Real elevation-granting UX + a second privileged process = its own deliverable; the consent framework it rides already ships | First spt-hosted agent that needs admin powers (or the instantiate-anywhere milestone) |
| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |
| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |
| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |
| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking reorg mid-acceptance; adapter window still open immediately after M7 closes | M8 planning — mint REQs first (rule 3) |
| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version fleet safe, old peers skip the new lines). Merged under the same epoch lease, evicted with the node, gated identically to instance feeds. The pump emits one label record per served subnet regardless of endpoint count; `node_status_rows` reads it to NAME an endpoint-less peer without counting it as an endpoint (`[0/0]` holds, no liveness effect). Chose option 2 over option 1 (trust-pin label at pairing) to avoid touching the SPAKE2 ceremony + un-dormanting warn-on-change (which couples with the REQ-SUBNET-7 machine_id re-pair overwrite) — option 2 is also live-updating on hostname change | M8 criterion-5 acceptance run (HFENDULEAM↔kitsubito, 2026-06-07; HF at `[0/0]` showed bare uuid to kitsubito) | Cosmetic; deferred per user decision 2026-06-07, fixed 2026-06-08 (user picked option 2) | DONE (real-hardware verify rides the next acceptance/deploy pass) |
| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred "option 1")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but not instant); (b) a peer that stops WITHIN a cadence of pairing is never named (its label never gossiped). FIX: capture the peer hostname **at pairing → the persistent trust-pin label**. Needs (1) BIDIRECTIONAL pairing intro — the joiner→responder `NodeIntro` (label+machine_id) already exists; add responder→joiner so BOTH sides capture each other's hostname; (2) wire `trust.record`'s `label` = peer hostname at both pin sites (`pairing/wire.rs` ~324 responder, ~435 initiator, both currently `None`); (3) handle the warn-on-change coupling with the REQ-SUBNET-7 machine_id re-pair overwrite (a legit machine reinstall = same machine_id, new key → repin, not a silent fail). **NOTE — the OFFLINE-loses-name half was a real bug, FIXED 2026-06-08** (not this deferred row): a *learned* node_label was being silence-decayed by `evict_nodes`, so an offline trusted member lost its name; `evict_nodes` now keeps labels (silence ≠ forget) and only an explicit `spt subnet prune` drops them (`evict_node_labels`). This row is the remaining INSTANT + NEVER-SEEN half | enlyzeam/kitsubito SPT_DEV diagnosis (user 2026-06-08) | The instant/never-seen half of REQ-SUBNET-3; pairs with the "post-join address seeding" row (same ceremony-connection seam, seed label + address both sides) — touches SPAKE2, so its own focused milestone | Next session — mint REQ for the trust-pin-label capture + bidirectional intro |
| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer "serving subnet X?", answered from its own `AttachmentStore::is_detached` ∧ membership (the same signal the pump/responder gate on); the dispatcher classifies it by the `serve_probe` first-line field; `wansend::probe_node_serving` dials (proves reachable) THEN asks, so a detached-but-alive peer reads offline within a cadence. Probe LATENCY had shipped separately 2026-06-07 (`run_bounded` 2.5s + "Checking remote nodes…" notice). int evidence: `dispatcher_serves_a_subnet_serve_probe` (loopback E2E) | M8 criterion-5 acceptance run (HFENDULEAM↔kitsubito, 2026-06-07) | Real liveness-model fix on accepted M7 hybrid-liveness; deferred to keep acceptance moving (user decision 2026-06-07) | DONE (criterion-5 detach-flip now GREEN; real-hardware re-verify rides the next acceptance/deploy pass) |
| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures) unlocking transitive trust (seed-holder = member = trusted) + transitive gossip relay + roster propagation; MUST re-solve eviction/epoch-lease under relay (supersedes KH 4.10's "no transitive gossip" safety assumption). ADR-level; mint REQ-MESH-* first. Folds in the deferred pairing-time hostname capture + post-join address seeding (same roster/ceremony seam) | A→B→C invisibility test (user 2026-06-08) — confirmed two-gap design limitation, not a code defect | **DESIGN RATIFIED 2026-06-08** (grill): symmetric **seed-proof membership** + **roster-only relay** (rows stay own-authored, no third-party relay → KH 4.10/7.5 PRESERVED, not superseded). Full design = **ADR-0017**; reqs = **REQ-MESH-1..6** (inactive); plan = `SUBNET-MESH-PLAN.md`; glossary = CONTEXT §Pairing & trust. Folds in the pairing-time hostname-capture + post-join address-seeding rows (roster fields). Next: plan-phase over REQ-MESH-* | Next milestone — design done 2026-06-08; build pending plan-phase |
| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual "Apply and observe" (`DEBUG-ROLLOUT.md`); the watcher's one genuinely new surface is a node status-query wire leg (no-fetch `{channel, applied_version, last_outcome}` over the existing update handshake) — assembly otherwise | First multi-node debug rollout where hand-walking nodes is the bottleneck, or M8 update polish |
| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a maintainer seeds the first node via the release-publish path). The installer DOES fetch from GitHub (`github.com/SaberMage/spt-releases/releases/latest/download/<asset>` + `SHA256SUMS`, `installer/install.sh:57-73`) but only at install time, outside the daemon's signed-release gate, and never re-runs. FIX = a new **origin-fetch leg** that downloads the per-platform artifact + its `<asset>.release.json` (the `SignedRelease` metadata) from the release origin and feeds them through the **SAME** `plan_verified` gate (`verify_metadata` two-key signature/channel/expiry/rollback + `verify_artifact` SHA-256, `update.rs:157`/`release.rs`), then `cache.stage()` — after which the EXISTING consent-notif / `spt update apply` flow is unchanged. So it reuses ~all the machinery; it only adds an HTTPS GET from the origin as an alternate SOURCE to the P2P `request_update` pull. Surface: `spt update fetch [--channel <ch>] [--tag vX.Y.Z]` (default: latest on the node's pinned channel); optionally a daemon-config `origin_check` cadence so a lone node self-bootstraps alongside P2P, then re-propagates to peers normally. **Key design note:** the daemon has **zero HTTP surface today** — all transport is iroh QUIC; `reqwest` is only a transitive dep (via iroh), not reachable. This leg needs a deliberate HTTPS-client decision (add `reqwest`/a minimal client as a direct dep, vs shell out to `curl`/`Invoke-WebRequest` like the installer — fragile). Honor the channel pin (`release-keys.json`) + the monotonic rollback floor; configurable repo (mirror `SPT_INSTALL_REPO`). The two-key signed-release anchor keeps the GitHub transport **untrusted-but-verified** (strictly stronger than the installer's HTTPS+SHA256SUMS-only first fetch). Mint **REQ-UPD-7** (origin-source fetch) first. | Update subsystem v1 (ADR-0016, REQ-UPD family) — P2P-pull only by design | v1 update flow is peer-propagation by design (seed one node, the fleet converges by gossip); origin-fetch is additive and the maintainer seed path covered bootstrap so far. No HTTP client is wired into the daemon yet | First isolated / first-in-fleet node that must update with no peer to pull from; or making `spt update` self-sufficient for solo / single-node users (user flagged the gap 2026-06-08) |
| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect("bind broker")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-09); same code passed clean on rerun → flake, not a logic bug. FIX = make the test bind deterministic: unlink-before-bind, OR abstract-namespace socket (Linux `@`-prefixed, no filesystem entry), OR per-test `TempDir` isolation so each bind has a guaranteed-fresh path. Applies to every `served_broker`/socket-name test helper, not just applyhost | Triaged during v0.3.2 release (2026-06-09) — flake, shipped over it | Non-blocking: a real flake that taxes CI (one red cycle + rerun per hit) but never gates a correct build; hardening is test-infra work, not product. Held to keep the v0.3.2 release moving | **Restoration D1/D7** (`RESTORATION-PLAN.md`) — D1/D7 add real process-spawn tests on the kitsubito runner; fix the bind determinism there rather than fighting flakes through the milestone (M8 is complete) |
| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a release applies the single for its OWN platform, then can't re-stage the same version as a set → has no Windows/other-platform artifact to SERVE peers over P2P. Surfaced live in the v0.3.2 cross-platform rig verify (2026-06-09): kitsubito staged+applied the v6 single, then its v6-set re-fetch was refused; unblock was a manual `releases/` cache-clear before re-fetch. Counter vs semver is working as designed (counter = trust spine, semver = cosmetic) — the gap is purely shape-blind: the floor compares version only, ignoring single-vs-set. FIX options: (a) allow a same-version re-fetch when staged shape is `single` and an available `set` is strictly-richer (shape-upgrade allowance), or (b) seed-server flow stages the SET before applying its own single, or (c) a `--force`/`--restage` flag. NOTE — only bites the one migration hop onto 0.3.2; once a node is on 0.3.2 with a fresh cache, future releases are vN+1 and stage clean. Mint a REQ before fixing | v0.3.2 cross-platform rig verify (2026-06-09; user-flagged via session watch) | Transitional-only: the workaround (cache-clear before staging the set) is reliable and was used to complete the v0.3.2 fleet roll; a code fix only saves the manual clear on same-version shape upgrades, which won't recur after the 0.3.2 hop | A future release that again changes staged-artifact SHAPE at a same/non-incrementing counter, or making `spt update fetch` self-sufficient for solo seed-nodes (pairs with the origin-source bootstrap row above) |
| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarantined bad version. The supervisor rescues the **brain** (it never execs `current_exe` while a `RolledBack` record stands), but an OS-service / autostart / `ensure_running` relaunch execs `current_exe` **as the broker** — and **if the bad binary panics before broker bind, the node is down until manual intervention** (record-driven selection cannot save a binary that won't boot). FIX = a calm-path reconcile: on the next clean `apply` (or a broker self-reconcile once stable), rename the good bytes over `current_exe` so the on-disk seat matches the running selection. Deliberately NOT a failure-time rename | Restoration D6-2 (ADR-0018 Q7, open-call-3, 2026-06-10) — record-driven selection rescues the brain; the on-disk seat reconcile defers | The reboot-survives-via-record path is correct NOW for the common case (broker boots, supervisor re-selects the good binary); the residual only bites when the broker process itself re-execs the quarantined `current_exe` and that binary won't even bind. Failure-time rename is the brittle path the design rejected; the calm-path reconcile needs the next clean-apply seam | First field rollback where the broker (not just the brain) restarts onto the quarantined on-disk bytes — or the D7 process-survival proof exercising a broker re-exec after rollback |
| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update apply --force`, or a `spt update unquarantine <version>`) that clears the `RolledBack` marker and re-opens the apply for that version — a deliberate, operator-gated act, never automatic | Restoration D6-2 (ADR-0018 Q7, note-i, 2026-06-10) — quarantine ships; the override defers | The natural N+1 clear covers the common "the next release fixes it" path; the override only matters when the SAME version must be retried after an environmental fix. Operator-gated re-apply is a small CLI + record-clear, deferrable to the alarm/adapter era | First node permanently allergic to a genuinely-good version (environmental rollback), or the M8 update-CLI polish |
| **Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)**: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers make the brain wait on a QUIC operation to a dead peer with no bound of their own, so the brain only escapes via its own deadline (a 30s stall + a full pump restart each time). FIX = the broker must never make a brain wait unbounded on a QUIC op — bound those handlers broker-side (a QUIC-level timeout that returns an Error event promptly), so a black-holed peer fails the single `peer_step` cleanly (ordinary per-peer abort + redial) without poisoning the client or forcing a pump restart. The broker is RESIDENT by design (CONTEXT §"broker update frequency: rare"), so this cannot reach the deployed fleet without a broker-update path — hence deferred to the next planned broker-update batch rather than riding the brain-only v0.4.1 roll the A-half ships on. **Added motivation (Windows, KH 7.6):** the A-half's reader-thread carrier leaks ONE `pump-ipc-reader` thread per wedge event — on a supervised restart the old reader is left blocked in `read_frame` holding its `RecvHalf`, and interprocess split halves share one OS handle (closes only when BOTH drop), so on a named pipe the broker may never signal the disconnect and the old reader can park indefinitely (especially while the broker itself is wedged on the dead-peer QUIC await — the very thing B fixes). Bounded (one thread per actual wedge, not per restart round — the post-restart conn cache is fresh, so the dead peer is re-dialed, not re-wedged on a cached dead conn), but B is the real cure: bounding the broker's QUIC await makes the brain's read return promptly, so the reader thread is never left parked | Pump-stall diagnosis 2026-06-11 (doyle ruling A-now / B-deferred) | The brain-side A-half fixes the fleet NOW via the brain-only update machinery (v0.4.1 candidate) and makes the wedge self-heal in 5s instead of 2.2h; B is the deeper correctness fix but only reachable through the rare broker-update path. Mint a REQ (broker-side QUIC-op bound) before building | Next planned broker-update batch (folds in with any other resident-broker change) |
| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's "remind me at 3pm" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today** — `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; the actual "wait until target, fire the TIMED PULSE" timer lives in the **legacy owl listener in-memory** (BROKER-BRAIN-SPLIT-RESTORATION §7). Building the one-shot deadline machinery in the restoration milestone would ship **untested dead code** (violates activate-don't-pre-fail). FIX = port alarms into the daemon as a durable scheduler that rides the Q4 one-shot rule (persist target-time at creation, fire-if-due on every brain start, survive both crash and update). The periodic-loop half of Q4 (anchor+interval derive) DOES ship in restoration D5; only the one-shot machinery defers here. | Restoration milestone D5 (ADR-0018 Q4/V3, 2026-06-09) — rule fixed, machinery deferred | The daemon has no one-shot consumer to exercise it; the periodic-deadline mechanism (the phase-significant pulse loop) ships in D5 and proves out the disk-anchored-deadline pattern the alarm port will reuse | The alarm port — porting the legacy in-memory alarm timer into the daemon as a durable scheduler (first real one-shot consumer); reuses the D5 disk-deadline pattern + the Q4 one-shot rule |
| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **autonomous file-watch→publish freshness nudge** (a watcher that re-projects + publishes when the log grows on its own, vs today's pull/push-driven publishes) — CONTEXT.md "session digest" anticipates this as "an incremental normalize story (design-phase concern)"; (b) the **Option-C opt-in persistence re-home** (the byte-feed engine drove per-turn persistence off `pty_digest.persist`, now deleted; re-homing it onto a `[history]`/digest config seam now = a manifest surface with nothing reading it). Building either now = a speculative realtime subsystem with no reader (the M9-PLAN §9 "no new realtime subsystem" watch-item). | M9 Wave 3 / G3 (doyle ruling 2026-06-13) — snapshot-pull is the G3 bar, delta-stream contract ships | Both ride a live consumer: the digest works topology-independently TODAY (pull projects real content; a subscriber follows and gets push-driven deltas). Autonomous freshness only matters when a frontend pane streams it continuously; Option-C only matters when something reads the coarse activity log. Deferring honors CONTEXT.md's anticipated staging, not a scope cut | The **consuming-frontend milestone** that streams the digest live (Shell M10 drive/sensory pane, the GUI "latest session output" pane, or the Gateway agent-window) — wire the file-watch nudge + re-home Option-C onto a digest config seam then, with a real reader to exercise both |
| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:"owl_message"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceable data model this milestone, with no consumer that distinguishes the subtypes yet. Splitting the tap into honest subtypes (true peer message vs notif vs shell-context) means classifying the frame at the delivery seam. | M10 Wave 3 / gate (doyle ruling 2026-06-13, flag 2 ACCEPTED with this tracked condition) | Same logic as the M9 freshness deferral above: refinement only matters when a surface reads + renders the context-injection category by subtype (GUI collapse/expand, or the echo-reads-digest delta loop). Coarse-but-honest labeling is correct for a data-model-only milestone | The **consuming-frontend / echo-reads-digest milestone** — split the `emit`-seam tap by frame type (peer message vs notif vs shell-context) when a reader renders the subtypes |


DEFERRED.md is a markdown table. Append a W2.5 row:


cd "C:\Users\decid\Documents\projects\spt-core" && cat >> docs/DEFERRED.md <<'EOF'
| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ "attached" tri-state** + the **"Kick `<node>` and attach"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — "who is attached to endpoint E's PTY, by which node/surface" — derive from the `presence.rs` PresenceLog conn connect/disconnect if sound, else a new query op (today `presence.rs` has snapshots/most_recently_active/resolve but no per-PTY attach owner); (2) a **force-detach/kick wire op** with its OWN KNOWN-HAZARDS treatment — the PTY-ownership invariant (KH attach-lifecycle: detach never kills the session) + a who-may-kick auth gate — this hazard-zone surface is the reason it is its own slice, not a picker-wave rider; (3) picker: the blue tri-state + the Kick option (status becomes online/attached/offline; Kick is EXCLUSIVE to already-attached endpoints). | M12 W2 design ruling (doyle/operator 2026-06-14, M12-W2-RULING.md Q1) | A new broker MUTATION surface in the PTY-ownership hazard zone earns its own focused JIT plan + design-check + gate; bundling it into the picker UX wave would mix a hazard-zone broker op with front-end work. W2 ships the picker over surfaces that exist | **Immediately after W2 lands** (W2.5) — own JIT plan + doyle design-check (like W2/W3) + own gate; mint REQ-* first (rule 3) |
EOF
echo "appended EXIT=$?"


appended EXIT=0


Now run the sweep — picker tests first.


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


running 0 tests

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

     Running tests\notify_shell_e2e.rs (target\debug\deps\notify_shell_e2e-69c19f9331e4b703.exe)

running 0 tests

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

     Running tests\oneliner_e2e.rs (target\debug\deps\oneliner_e2e-5adc759848e087b4.exe)

running 0 tests

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

     Running tests\quickstart_e2e.rs (target\debug\deps\quickstart_e2e-609c19c7e503bd64.exe)

running 0 tests

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

     Running tests\release_verify_e2e.rs (target\debug\deps\release_verify_e2e-74541e2bea7eb559.exe)

running 0 tests

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

     Running tests\shell_e2e.rs (target\debug\deps\shell_e2e-efc813acb26b0dd3.exe)

running 0 tests

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

     Running tests\shell_sleepwake_e2e.rs (target\debug\deps\shell_sleepwake_e2e-6e5abca434940e19.exe)

running 0 tests

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


Need the bin unit tests specifically:


cd "C:\Users\decid\Documents\projects\spt-core" && cargo test -p spt --bin spt picker:: 2>&1 | tail -45


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.53s
     Running unittests src\main.rs (target\debug\deps\spt-3efb10b0dcc4d9ed.exe)

running 25 tests
test picker::model::tests::back_unwinds ... ok
test picker::model::tests::category_ring_wraps ... ok
test picker::model::tests::adapter_tree_nests_profiles ... ok
test picker::model::tests::confirm_options_status_dependent ... ok
test picker::model::tests::cursor_clamps ... ok
test picker::model::tests::resume_outcome_bakes_session ... ok
test picker::model::tests::create_outcome_bakes_run ... ok
test picker::model::tests::kind_routes ... ok
test picker::model::tests::reenter_create_keeps_or_clears_id ... ok
test picker::model::tests::confirm_terminal_routes ... ok
test picker::model::tests::resume_title_render ... ok
test picker::model::tests::visible_partitions_and_sorts ... ok
test picker::shortcut::tests::bakes_resume_and_action ... ok
test picker::model::tests::prefill_jumps_to_create ... ok
test picker::shortcut::tests::bakes_create_attach_by_default ... ok
test picker::model::tests::status_square_is_online_offline_only ... ok
test picker::model::tests::filter_narrows ... ok
test picker::shortcut::tests::sentinel_detection ... ok
test picker::view::tests::kind_buffer ... ok
test picker::view::tests::resume_buffer_lists_sessions ... ok
test picker::view::tests::adapter_tree_buffer ... ok
test picker::view::tests::confirm_buffer_options ... ok
test picker::view::tests::pick_empty_category_hint ... ok
test picker::view::tests::pick_buffer_has_squares_and_description ... ok
test picker::shortcut::tests::write_create_update_refuse ... ok

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


All 25 picker tests green. Run clippy + traceable check (will show missing doc-stage tags).


cd "C:\Users\decid\Documents\projects\spt-core" && echo "=== traceable check ===" && traceable-reqs check 2>&1 | grep -iE 'RUN-PICKER|RUN-SHORTCUT|FAIL|missing|error|EXIT|pass' | head -40; echo "rc=$?"


=== traceable check ===
manifest_error: cannot parse traceable-reqs.toml (TOML parse error at line 476, column 811
rc=0


cd "C:\Users\decid\Documents\projects\spt-core" && cargo clippy -p spt --all-targets 2>&1 | grep -iE 'warning|error' | head -30; echo "clippy done"


clippy done


clippy clean. TOML parse error at 476 — my edit. Inspect:


474	[[requirements]]
475	id = "REQ-RUN-SHORTCUT"
476	title = "`cc-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `cc-<id>` launcher at the project root baking the current selection's non-interactive `spt endpoint run` flags (terminal actions only: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). The script is the CURRENT OS's native form — `.cmd` on Windows (NOT `.ps1`: default PATHEXT excludes `.ps1` so a bare/ext-less name never resolves one; `.cmd` is PATHEXT-resolvable — operator rev. 2026-06-14), POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). The generated header documents the invocation reality (cmd.exe bare `cc-<id>` in the project dir / PowerShell `.\cc-<id>` / Unix `./cc-<id>`; a truly-bare `cc` = a PATH-installed launcher, `/spt:setup`'s job). Overwrite is SENTINEL-guarded: the generator writes + checks a generated-by header marker — it overwrites its own prior output freely (\"Update `cc-<id>` shortcut\"), but REFUSES + warns if a same-named file lacks the sentinel (never clobber a user file). Requires the additive `--create` flag on `Run{}` (the default-fresh made explicit; N-1-safe)."
477	required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: shortcut content + sentinel create-vs-update as unit; flag→action mapping unit)

[[requirements]]
id = "REQ-RUN-SHORTCUT"
title = "`cc-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `cc-<id>` launcher at the project root baking the current selection's non-interactive `spt endpoint run` flags (terminal actions only: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). The script is the CURRENT OS's native form — `.cmd` on Windows (NOT `.ps1`: default PATHEXT excludes `.ps1` so a bare/ext-less name never resolves one; `.cmd` is PATHEXT-resolvable — operator rev. 2026-06-14), POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). The generated header documents the invocation reality (cmd.exe bare `cc-<id>` in the project dir / PowerShell `.\cc-<id>` / Unix `./cc-<id>`; a truly-bare `cc` = a PATH-installed launcher, `/spt:setup`'s job). Overwrite is SENTINEL-guarded: the generator writes + checks a generated-by header marker — it overwrites its own prior output freely (\"Update `cc-<id>` shortcut\"), but REFUSES + warns if a same-named file lacks the sentinel (never clobber a user file). Requires the additive `--create` flag on `Run{}` (the default-fresh made explicit; N-1-safe)."
required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: shortcut content + sentinel create-vs-update as unit; flag→action mapping unit)


`.\cc` = invalid TOML escape (`\c`). Escape the backslash:


Edit

cmd.exe bare `cc-<id>` in the project dir / PowerShell `.\cc-<id>` / Unix `./cc-<id>`; a truly-bare `cc` = a PATH-installed launcher, `/spt:setup`'s job

cmd.exe bare `cc-<id>` in the project dir / PowerShell `.\\cc-<id>` / Unix `./cc-<id>`; a truly-bare `cc` = a PATH-installed launcher, `/spt:setup`'s job


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

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

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

required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: shortcut content + sentinel create-vs-update as unit; flag→action mapping unit)

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


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && traceable-reqs check 2>&1 | grep -iE 'RUN-PICKER|RUN-SHORTCUT|FAIL|missing|parse|error|complete|pass|✓|✗' | head -40


200 requirements: 198 complete, 2 incomplete, 2 findings
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
  [OK] REQ-HAZARD-ENVELOPE-PARSER-SAFE  required: [impl, unit]  stages: -doc +impl +unit -int
       Two-slice envelope parser is panic-free and tolerant (4.2)
       Grace-period wait completes before composing INIT_SIGNOFF (1.1)
       Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
       Adapter profiles — sparse leaf-replace overlays (shipped + local), composite <adapter>:<profile> addressing, shadow-refusal, tighten-only consent floors
       File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = "rel/path" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings].
       Member roster: node-level union-merge grow-set (per member: pubkey, label, machine_id, last-known address, last-seen — NOT the seed), the discovery directory the mesh dials by. Seeded IN FULL at pairing (seed-holder hands joiner the whole current roster, incl. offline members — folds in deferred pairing-time hostname capture + post-join address seeding); each node authors its own entry stamped with its lease_epoch, merged strictly-greater-wins (the node_label lease); exchanged only over seed-proof'd member connections; forgery-inert (a fake entry names a pubkey that still can't seed-proof). Removal needs a TOMBSTONE — a per-pubkey revoked marker that propagates, dominates the entry, gates admission (seed-proof ∧ ¬tombstoned), and prevents reinsert; cleared by a completed re-pair of that pubkey. Persists through silence (offline member keeps its entry).
       Listener stream stdout emits EVENT envelope lines (sister-format, ADR-0001): parse the __REPLY_TO__ frame, pass pre-formed typed envelopes through verbatim (no double-wrap), compose <EVENT type="msg" from=…> otherwise, chunk oversized lines into EVENT-PART
       cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
       `spt rc <id>` — user CLI attaching a local terminal to a broker-held PTY, reusing the cross-node attach machinery (attach.rs request_attach → send_attach_input pump, spt-net AttachRecord codec); local attach is the degenerate single-node case of the cross-node path (rides REQ-TERM-3 byte-stream streaming). Read-only `--view` (watch, no stdin forwarded). Clean detach that does NOT terminate the broker-held session (KNOWN-HAZARDS: PTY ownership stays with the broker; no termination on detach). Explicit detach keybind that cannot collide with harness passthrough input (legacy capsule used a ctrl-b prefix); documented. ConPTY DSR auto-answer in the attach reader (hazard 5.5).
  [--] REQ-RUN-PICKER  required: [doc, impl, unit]  stages: -doc +impl +unit -int
  [--] REQ-RUN-SHORTCUT  required: [doc, impl, unit]  stages: -doc +impl +unit -int
       Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [must] missing_stage REQ-RUN-PICKER stage=doc — Requirement is missing documentation evidence
  [must] missing_stage REQ-RUN-SHORTCUT stage=doc — Requirement is missing documentation evidence


impl + unit ✓ for both; only `doc` stage missing. Find where recent doc-stage tags live (precedent):


Found 1 file
CONTEXT.md


Grep

doc->REQ-MANIFEST-5|doc->REQ-RC-1|doc->REQ-HOST-RUN|endpoint run|## .*[Bb]ringup|spt rc


83:<!-- [doc->REQ-MANIFEST-5] -->
316:**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):
317:[Omitted long matching line]


308	**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —
309	- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;
310	- owner **suspended** → revive the owner, then online the shell;
311	- owner **active** elsewhere → attach/online the shell;
312	- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).
313
314	The same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.
315
316	**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):
317	The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking the same stream substrate) is the GUI's terminal pane and the future Android-gateway shape — the bridge is just another `rc`-class client.
318	_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.
319
320	## Terminal wrapper
321
322	The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned "capsule" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface. **The session-surface *mechanism* is delivered in M3a (`crates/spt-term`); the supervisor *process* (term-daemon) is delivered in M3b (`crates/spt-daemon`, 2026-06-03).**
323
324	**term-daemon** (process supervisor):
325	A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*
326
327	**session surface** (library abstraction):
328	The trait spt-core exposes over "anything you can write input to and read output from" — delivered in M3a as `spt_term::SessionSurface` (`write_input` + `resize`, with `send_keys`/`send_line` as first-class injection). Native PTY (`spt_term::PtySession`: ConPTY on Windows 10+, `forkpty(3)` on Unix, via the `portable-pty` crate) is the day-one implementation. The abstraction generalizes so future implementations — network-attached surfaces (for `spt-node` remote control), GUI text panes, etc. — are uniform to consumers. Pre-Windows-10 (winpty) is explicitly dropped. **OS-neutral by contract (Spike #4/#5):** the trait bakes in neither `forkpty`'s raw-ordered-pipe nor ConPTY's repaint-on-resize screen-buffer semantics; output is a plain byte stream (`spt_term::OutputStream`, bounded backpressure) with OS-independent replay.
329
330	**input injection**:
331	Two granularities, both first-class. `send-keys` — raw byte stream including escape sequences (e.g. `Ctrl-C`). `send-line` — cooked line-level input flushed on newline (the common case; matches how user input reaches an agent prompt).
332
333	**scrollback**:
334	In-memory ring buffer per session for v1. **On-disk spillover for long sessions is an intended, tracked feature** (deferred past v1, not forgotten) — see `docs/DEFERRED.md`.
335
336	**live activity buffer (session digest)**:
337	A rolling, human-glanceable view of an endpoint's **recent context** — *what the agent did* (recent user turns + the agent output between them, with **tool-usage sprints collapsed**, e.g. `<endpoint_id> used: **Write** `dir/file.txt` · **Bash** `first ~25 cmd chars` · […]`) **and** *what spt fed the agent* (**context-injection** entries — session-start psyche download, echo-commune mirror, incoming owl messages — collapsed by default, expandable). One time-ordered, glanceable timeline. **How much** is shown (window depth, arg-truncation, sprint-collapse) is the adapter's declared preference, consumer-overridable — *not* a fixed spt-core count. Distinct from both the **raw scrollback ring** (unparsed bytes) and full **transcript history** (the conversation behind echo communes/resume): its job is *"what is this agent's recent context, at a glance,"* not conversation replay.

**Wake resolution (state-keyed):** find any *reachable* instance of the owner endpoint on the subnet, then —
- owner **dormant** (still running) → do nothing to the endpoint, just bring the shell online;
- owner **suspended** → revive the owner, then online the shell;
- owner **active** elsewhere → attach/online the shell;
- **no reachable instance** (home node offline) → no-op, **unless** the owner's **`shell_wake_spawn_anywhere`** settings flag is set, which pre-consents shells to **fresh-spawn the owner on any available node** to wake it (the flag *is* the consent for this otherwise-deferred instantiate-anywhere path).

The same `spt endpoint shutdown` / `api owner-shutdown` / avatar-wake machinery composes into a full sleep/wake cycle driven from either end.

**remote attach (`spt rc`)** (ratified 2026-06-12 — own milestone, rides the built R-TERM substrate):
The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking the same stream substrate) is the GUI's terminal pane and the future Android-gateway shape — the bridge is just another `rc`-class client.
_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.

## Terminal wrapper

The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned "capsule" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface. **The session-surface *mechanism* is delivered in M3a (`crates/spt-term`); the supervisor *process* (term-daemon) is delivered in M3b (`crates/spt-daemon`, 2026-06-03).**

**term-daemon** (process supervisor):
A long-lived, one-per-machine daemon that owns the PTYs for all hosted sessions, multiplexes them (sessions addressable by name, tmux-style), and accepts attach/detach from frontends. A hosted agent session (a LiveAgent's Self, a ReadyAgent, etc.) runs *inside* a supervised PTY. **Headed** = a frontend is attached and rendering/driving the PTY; **headless** = the session runs with no frontend attached. The daemon's persistence is what lets a session keep running while unwatched and be re-attached "on a whim." *(M3b ✅ — `spt-daemon` hosts many M3a `spt-term` surfaces behind a versioned local IPC, split into a stable **broker** (holds the PTYs/children/sockets) + a restartable **brain** (all logic) so a brain swap leaves hosted sessions gapless; `spt-term` itself is deliberately single-surface-per-handle.)*

**session surface** (library abstraction):
The trait spt-core exposes over "anything you can write input to and read output from" — delivered in M3a as `spt_term::SessionSurface` (`write_input` + `resize`, with `send_keys`/`send_line` as first-class injection). Native PTY (`spt_term::PtySession`: ConPTY on Windows 10+, `forkpty(3)` on Unix, via the `portable-pty` crate) is the day-one implementation. The abstraction generalizes so future implementations — network-attached surfaces (for `spt-node` remote control), GUI text panes, etc. — are uniform to consumers. Pre-Windows-10 (winpty) is explicitly dropped. **OS-neutral by contract (Spike #4/#5):** the trait bakes in neither `forkpty`'s raw-ordered-pipe nor ConPTY's repaint-on-resize screen-buffer semantics; output is a plain byte stream (`spt_term::OutputStream`, bounded backpressure) with OS-independent replay.

**input injection**:
Two granularities, both first-class. `send-keys` — raw byte stream including escape sequences (e.g. `Ctrl-C`). `send-line` — cooked line-level input flushed on newline (the common case; matches how user input reaches an agent prompt).

**scrollback**:
In-memory ring buffer per session for v1. **On-disk spillover for long sessions is an intended, tracked feature** (deferred past v1, not forgotten) — see `docs/DEFERRED.md`.

**live activity buffer (session digest)**:
A rolling, human-glanceable view of an endpoint's **recent context** — *what the agent did* (recent user turns + the agent output between them, with **tool-usage sprints collapsed**, e.g. `<endpoint_id> used: **Write** `dir/file.txt` · **Bash** `first ~25 cmd chars` · […]`) **and** *what spt fed the agent* (**context-injection** entries — session-start psyche download, echo-commune mirror, incoming owl messages — collapsed by default, expandable). One time-ordered, glanceable timeline. **How much** is shown (window depth, arg-truncation, sprint-collapse) is the adapter's declared preference, consumer-overridable — *not* a fixed spt-core count. Distinct from both the **raw scrollback ring** (unparsed bytes) and full **transcript history** (the conversation behind echo communes/resume): its job is *"what is this agent's recent context, at a glance,"* not conversation replay.


CONTEXT.md is the doc home (hand-authored glossary). Add a picker + shortcut subsection with both doc tags, beside the `spt rc` section:


Edit

_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.

## Terminal wrapper

_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.

**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):
<!-- [doc->REQ-RUN-PICKER] -->
The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no
`--adapter`/`--id`) opens an in-process **ratatui picker**; the **flagged** form is the
non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>
--start|--attach|--view`), untouched — a picker selection bakes exactly that path. **Layer 1**
picks the kind (*Create new* | *Pick existing*). **Create-new** chooses a registered
`kind="harness"` adapter with its shipped+local **profiles tree-nested**, then a
charset-validated id, then starts. **Pick-existing** selects a **category** (←→ over
`[<cwd-project> | Local node | Subnet]`), endpoints **grouped + alphabetically sorted** with a
**status square** (online green ■ / offline gray ▢ — the blue *attached* tri-state + *Kick* are
**W2.5**, a dedicated broker attach-presence slice), **type-to-filter** (`/`), a pinned keybind
legend, and a **two-pane** right-half description (harness `adapter:profile` · best-effort
project history newest→oldest · `endpoint description`). The **confirm** layer offers
status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally
(remote) · Change-harness-adapter (offline) · Fork · **Resume-from-history** (offline+LOCAL
only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes
that session id). **Invariant:** the picker is a pure front-end — every terminal action routes
through the one bringup core (`spt endpoint run` / the rc pump), never a second path. A single
action enum is the source of truth so a future tap-mode (phone PTY) layers on without
re-coupling to keybinds.
_Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.

**`cc-<id>` shortcut** (picker `s` keybind, M12-W2):
<!-- [doc->REQ-RUN-SHORTCUT] -->
From any pre-start options set, `s` writes (or updates) a **`cc-<id>` launcher** at the project
root that bakes the current selection's **non-interactive** flags (terminal actions only:
adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches —
Kick/Instantiate/Change-adapter/Fork — are not bakeable). The launcher is the **current OS's
native form**: a **`.cmd`** on Windows (the default `PATHEXT` excludes `.ps1`, so a bare name
never resolves one; `.cmd` is PATHEXT-resolvable), a POSIX **`sh`** (`chmod +x`) on Unix.
**Invocation reality** (documented in the generated header): cmd.exe bare `cc-<id>` in the
project dir · PowerShell `.\cc-<id>` · Unix `./cc-<id>`; a *truly-bare* `cc` on `PATH` is a
PATH-installed launcher (`/spt:setup`'s job), not this project-root shortcut. **Overwrite is
sentinel-guarded:** the generator writes + checks a generated-by header marker — it overwrites
its own prior output freely, but refuses (with a warning) a same-named file that lacks the
sentinel, never clobbering a user file.
_Avoid_: `.ps1` shortcuts (PATHEXT won't find them); clobbering a non-generated file.

## Terminal wrapper


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

_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.

## Terminal wrapper

_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.

**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):
<!-- [doc->REQ-RUN-PICKER] -->
The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no
`--adapter`/`--id`) opens an in-process **ratatui picker**; the **flagged** form is the
non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>
--start|--attach|--view`), untouched — a picker selection bakes exactly that path. **Layer 1**
picks the kind (*Create new* | *Pick existing*). **Create-new** chooses a registered
`kind="harness"` adapter with its shipped+local **profiles tree-nested**, then a
charset-validated id, then starts. **Pick-existing** selects a **category** (←→ over
`[<cwd-project> | Local node | Subnet]`), endpoints **grouped + alphabetically sorted** with a
**status square** (online green ■ / offline gray ▢ — the blue *attached* tri-state + *Kick* are
**W2.5**, a dedicated broker attach-presence slice), **type-to-filter** (`/`), a pinned keybind
legend, and a **two-pane** right-half description (harness `adapter:profile` · best-effort
project history newest→oldest · `endpoint description`). The **confirm** layer offers
status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally
(remote) · Change-harness-adapter (offline) · Fork · **Resume-from-history** (offline+LOCAL
only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes
that session id). **Invariant:** the picker is a pure front-end — every terminal action routes
through the one bringup core (`spt endpoint run` / the rc pump), never a second path. A single
action enum is the source of truth so a future tap-mode (phone PTY) layers on without
re-coupling to keybinds.
_Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.

**`cc-<id>` shortcut** (picker `s` keybind, M12-W2):
<!-- [doc->REQ-RUN-SHORTCUT] -->
From any pre-start options set, `s` writes (or updates) a **`cc-<id>` launcher** at the project
root that bakes the current selection's **non-interactive** flags (terminal actions only:
adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches —
Kick/Instantiate/Change-adapter/Fork — are not bakeable). The launcher is the **current OS's
native form**: a **`.cmd`** on Windows (the default `PATHEXT` excludes `.ps1`, so a bare name
never resolves one; `.cmd` is PATHEXT-resolvable), a POSIX **`sh`** (`chmod +x`) on Unix.
**Invocation reality** (documented in the generated header): cmd.exe bare `cc-<id>` in the
project dir · PowerShell `.\cc-<id>` · Unix `./cc-<id>`; a *truly-bare* `cc` on `PATH` is a
PATH-installed launcher (`/spt:setup`'s job), not this project-root shortcut. **Overwrite is
sentinel-guarded:** the generator writes + checks a generated-by header marker — it overwrites
its own prior output freely, but refuses (with a warning) a same-named file that lacks the
sentinel, never clobbering a user file.
_Avoid_: `.ps1` shortcuts (PATHEXT won't find them); clobbering a non-generated file.

## Terminal wrapper

The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking the same stream substrate) is the GUI's terminal pane and the future Android-gateway shape — the bridge is just another `rc`-class client.

+**spt-hosted bringup picker (`spt endpoint run`)** (M12-W2):

+The user-facing bringup flow for spt-hosted endpoints. **Bare `spt endpoint run`** (no

+`--adapter`/`--id`) opens an in-process **ratatui picker**; the **flagged** form is the

+non-interactive bringup path (`--adapter <a[:profile]> --id <id> --create|--resume <session>

+--start|--attach|--view`), untouched — a picker selection bakes exactly that path. **Layer 1**

+picks the kind (*Create new* | *Pick existing*). **Create-new** chooses a registered

+`kind="harness"` adapter with its shipped+local **profiles tree-nested**, then a

+charset-validated id, then starts. **Pick-existing** selects a **category** (←→ over

+`[<cwd-project> | Local node | Subnet]`), endpoints **grouped + alphabetically sorted** with a

+**status square** (online green ■ / offline gray ▢ — the blue *attached* tri-state + *Kick* are

+**W2.5**, a dedicated broker attach-presence slice), **type-to-filter** (`/`), a pinned keybind

+legend, and a **two-pane** right-half description (harness `adapter:profile` · best-effort

+project history newest→oldest · `endpoint description`). The **confirm** layer offers

+status-dependent options — Attach/Start/View (the rc pump / bringup) · Instantiate-locally

+(remote) · Change-harness-adapter (offline) · Fork · **Resume-from-history** (offline+LOCAL

+only — enumerates the per-endpoint session ledger, titles `<project> @ <ts> (…id5)`, resumes

+that session id). **Invariant:** the picker is a pure front-end — every terminal action routes

+through the one bringup core (`spt endpoint run` / the rc pump), never a second path. A single

+action enum is the source of truth so a future tap-mode (phone PTY) layers on without

+_Avoid_: a second bringup path; hard-coupling interaction to physical keybinds.

+**`cc-<id>` shortcut** (picker `s` keybind, M12-W2):

+From any pre-start options set, `s` writes (or updates) a **`cc-<id>` launcher** at the project

+root that bakes the current selection's **non-interactive** flags (terminal actions only:

+adapter[:profile] + id + create|resume + start|attach|view; the interactive-only branches —

+Kick/Instantiate/Change-adapter/Fork — are not bakeable). The launcher is the **current OS's

+native form**: a **`.cmd`** on Windows (the default `PATHEXT` excludes `.ps1`, so a bare name

+never resolves one; `.cmd` is PATHEXT-resolvable), a POSIX **`sh`** (`chmod +x`) on Unix.

+**Invocation reality** (documented in the generated header): cmd.exe bare `cc-<id>` in the

+project dir · PowerShell `.\cc-<id>` · Unix `./cc-<id>`; a *truly-bare* `cc` on `PATH` is a

+PATH-installed launcher (`/spt:setup`'s job), not this project-root shortcut. **Overwrite is

+sentinel-guarded:** the generator writes + checks a generated-by header marker — it overwrites

+its own prior output freely, but refuses (with a warning) a same-named file that lacks the

+sentinel, never clobbering a user file.

+_Avoid_: `.ps1` shortcuts (PATHEXT won't find them); clobbering a non-generated file.

The multiplatform terminal wrapper is the backbone for hosting agent sessions. Supersedes the sister project's planned "capsule" (which leaned on psmux). Architecturally a **process supervisor** with an abstract session surface. **The session-surface *mechanism* is delivered in M3a (`crates/spt-term`); the supervisor *process* (term-daemon) is delivered in M3b (`crates/spt-daemon`, 2026-06-03).**


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && traceable-reqs check 2>&1 | tail -5; echo "TRACEABLE_RC=$?"


       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
TRACEABLE_RC=0


cd "C:\Users\decid\Documents\projects\spt-core" && traceable-reqs check 2>&1 | grep -iE 'complete|incomplete|finding|RUN-PICKER|RUN-SHORTCUT' | head


200 requirements: 200 complete, 0 incomplete, 0 findings
       Grace-period wait completes before composing INIT_SIGNOFF (1.1)
       Member roster: node-level union-merge grow-set (per member: pubkey, label, machine_id, last-known address, last-seen — NOT the seed), the discovery directory the mesh dials by. Seeded IN FULL at pairing (seed-holder hands joiner the whole current roster, incl. offline members — folds in deferred pairing-time hostname capture + post-join address seeding); each node authors its own entry stamped with its lease_epoch, merged strictly-greater-wins (the node_label lease); exchanged only over seed-proof'd member connections; forgery-inert (a fake entry names a pubkey that still can't seed-proof). Removal needs a TOMBSTONE — a per-pubkey revoked marker that propagates, dominates the entry, gates admission (seed-proof ∧ ¬tombstoned), and prevents reinsert; cleared by a completed re-pair of that pubkey. Persists through silence (offline member keeps its entry).
  [OK] REQ-RUN-PICKER  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-RUN-SHORTCUT  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)


200/200 complete, both REQs OK. Now full-workspace clippy -D + test suite (the gate bar).


cd "C:\Users\decid\Documents\projects\spt-core" && cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -15; echo "CLIPPY_DONE"


    Checking spt-proto v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-proto)
    Checking mock-adapter v0.6.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
    Checking spt-store v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
    Checking spt-msg v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
    Checking spt-runtime v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Checking spt-term v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Checking spt-net v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-live v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    Checking spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking xtask v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Checking spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.95s
CLIPPY_DONE


Clippy clean workspace-wide. Now the full test suite:


cd "C:\Users\decid\Documents\projects\spt-core" && cargo test --workspace 2>&1 | grep -iE 'test result|error\[|FAILED|panicked|running [0-9]+ tests' | tail -60; echo "TEST_DONE"