240	    /// harness binds to exactly it. This is the non-interactive core (the
241	    /// interactive picker lands in a later wave); the flags cover every terminal
242	    /// action so a `cc-<id>` shortcut can bake a fully non-interactive launch.
243	    Run {
244	        /// The harness adapter to host: `<adapter>[:profile]` (must be a
245	        /// registered `kind="harness"` adapter on this node).
246	        #[arg(long)]
247	        adapter: String,
248	        /// The endpoint id to bring up (charset: alphanumeric, `-`, `_`).
249	        #[arg(long)]
250	        id: String,
251	        /// Resume a prior session id instead of minting a fresh one.
252	        #[arg(long)]
253	        resume: Option<String>,
254	        /// Start the endpoint and return immediately (no attach).
255	        #[arg(long, conflicts_with_all = ["attach", "view"])]
256	        start: bool,
257	        /// Attach a local terminal after bringup (the default action).
258	        #[arg(long, conflicts_with_all = ["start", "view"])]
259	        attach: bool,
260	        /// Attach read-only after bringup (watch; forward no input).
261	        #[arg(long, conflicts_with_all = ["start", "attach"])]
262	        view: bool,
263	    },
264	    /// Fork an endpoint into another subnet as a NEW identity.
265	    ///
266	    /// Home subnets are immutable — fork is the cross-subnet move, never
267	    /// a re-home. Seeds the fork with a one-time copy of the source's
268	    /// mind (live + project tiers); the two diverge immediately (no
269	    /// ongoing sync). The source is untouched unless --delete-source.
270	    /// Same-node only today (one node holds one perch per name, so a
271	    /// local fork needs a new id).
272	    Fork {
273	        /// The source endpoint (must exist on this node).
274	        src: String,
275	        /// The fork's id (must differ from the source on the same node).
276	        new_id: String,
277	        /// The fork's home subnet — the target (must be a member).
278	        #[arg(long)]
279	        subnet: String,
280	        /// Delete the source endpoint (perch + tracked mind) after the copy.
281	        #[arg(long)]
282	        delete_source: bool,
283	    },
284	    /// Rest an endpoint cold (the suspend edge).
285	    ///
286	    /// The resting state machine's suspend edge. From dormant — or
287	    /// straight from active, in which case the final
288	    /// context save still fires first. Accepts a qualified `id@node` to
289	    /// suspend an instance on a paired peer.
290	    Suspend {
291	        /// The endpoint id (qualified `id@node` reaches a paired peer).
292	        id: String,
293	    },
294	    /// Wake a resting endpoint in place.
295	    ///
296	    /// Re-activates the existing seat (state's already there — no fresh
297	    /// spawn), resurfaces undismissed notifications, and requests an
298	    /// immediate context freshness pull from trusted peers. Accepts a
299	    /// qualified `id@node` for an instance on a paired peer.
300	    Wake {
301	        /// The endpoint id (qualified `id@node` reaches a paired peer).
302	        id: String,
303	    },
304	    /// Gracefully shut down an agent's own endpoint.
305	    ///
306	    /// Soft-stops the listener, then the suspend edge — the final context
307	    /// save fires and persistent shells cascade offline with it.
308	    Shutdown {
309	        /// The endpoint id (defaults to the session's own perch).
310	        id: Option<String>,
311	    },
312	    /// Soft-stop a perch (spool preserved).
313	    ///
314	    /// Removes the ready marker and unregisters the perch; the spool is
315	    /// preserved.
316	    Stop {
317	        /// Perch id to stop.
318	        id: String,
319	    },
320	    /// Rename an endpoint's logical id across its on-disk state.
321	    ///
322	    /// Rippled everywhere the id appears: the endpoint's perch dir, its
323	    /// nested companion/worker perches, and every record naming it.
324	    /// Refuses while the perch is live (stop it first).
325	    Rename {
326	        /// The endpoint's current (bare) id.
327	        old_id: String,
328	        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
329	        new_id: String,

    /// harness binds to exactly it. This is the non-interactive core (the
    /// interactive picker lands in a later wave); the flags cover every terminal
    /// action so a `cc-<id>` shortcut can bake a fully non-interactive launch.
    Run {
        /// The harness adapter to host: `<adapter>[:profile]` (must be a
        /// registered `kind="harness"` adapter on this node).
        #[arg(long)]
        adapter: String,
        /// The endpoint id to bring up (charset: alphanumeric, `-`, `_`).
        #[arg(long)]
        id: String,
        /// Resume a prior session id instead of minting a fresh one.
        #[arg(long)]
        resume: Option<String>,
        /// Start the endpoint and return immediately (no attach).
        #[arg(long, conflicts_with_all = ["attach", "view"])]
        start: bool,
        /// Attach a local terminal after bringup (the default action).
        #[arg(long, conflicts_with_all = ["start", "view"])]
        attach: bool,
        /// Attach read-only after bringup (watch; forward no input).
        #[arg(long, conflicts_with_all = ["start", "attach"])]
        view: bool,
    },
    /// Fork an endpoint into another subnet as a NEW identity.
    ///
    /// Home subnets are immutable — fork is the cross-subnet move, never
    /// a re-home. Seeds the fork with a one-time copy of the source's
    /// mind (live + project tiers); the two diverge immediately (no
    /// ongoing sync). The source is untouched unless --delete-source.
    /// Same-node only today (one node holds one perch per name, so a
    /// local fork needs a new id).
    Fork {
        /// The source endpoint (must exist on this node).
        src: String,
        /// The fork's id (must differ from the source on the same node).
        new_id: String,
        /// The fork's home subnet — the target (must be a member).
        #[arg(long)]
        subnet: String,
        /// Delete the source endpoint (perch + tracked mind) after the copy.
        #[arg(long)]
        delete_source: bool,
    },
    /// Rest an endpoint cold (the suspend edge).
    ///
    /// The resting state machine's suspend edge. From dormant — or
    /// straight from active, in which case the final
    /// context save still fires first. Accepts a qualified `id@node` to
    /// suspend an instance on a paired peer.
    Suspend {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Wake a resting endpoint in place.
    ///
    /// Re-activates the existing seat (state's already there — no fresh
    /// spawn), resurfaces undismissed notifications, and requests an
    /// immediate context freshness pull from trusted peers. Accepts a
    /// qualified `id@node` for an instance on a paired peer.
    Wake {
        /// The endpoint id (qualified `id@node` reaches a paired peer).
        id: String,
    },
    /// Gracefully shut down an agent's own endpoint.
    ///
    /// Soft-stops the listener, then the suspend edge — the final context
    /// save fires and persistent shells cascade offline with it.
    Shutdown {
        /// The endpoint id (defaults to the session's own perch).
        id: Option<String>,
    },
    /// Soft-stop a perch (spool preserved).
    ///
    /// Removes the ready marker and unregisters the perch; the spool is
    /// preserved.
    Stop {
        /// Perch id to stop.
        id: String,
    },
    /// Rename an endpoint's logical id across its on-disk state.
    ///
    /// Rippled everywhere the id appears: the endpoint's perch dir, its
    /// nested companion/worker perches, and every record naming it.
    /// Refuses while the perch is live (stop it first).
    Rename {
        /// The endpoint's current (bare) id.
        old_id: String,
        /// The new (bare) id — charset-validated; `:`/`@` are reserved.
        new_id: String,


1054	/// `spt endpoint run` brings up an spt-hosted harness endpoint (M12-W1).
1055	/// Resolves the harness adapter, spawns its `[session.self]` into a broker-held
1056	/// PTY (the harness self-registers its perch on bind), then starts, attaches, or
1057	/// views per the chosen terminal-action flag.
1058	// [impl->REQ-HOST-RUN-1]
1059	fn cmd_endpoint_run(
1060	    adapter: &str,
1061	    id: &str,
1062	    resume: Option<String>,
1063	    start: bool,
1064	    // `--attach` is the default terminal action; the bool is accepted for flag
1065	    // symmetry but `start`/`view` are what branch (attach = neither).
1066	    _attach: bool,
1067	    view: bool,
1068	) -> i32 {
1069	    // id charset (the harness binds to exactly this id via argv).
1070	    // [impl->REQ-HAZARD-ID-CHARSET]
1071	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
1072	        eprintln!("INVALID_ID:{id}: {e}");
1073	        return 2;
1074	    }
1075	    // Resolve the harness adapter option (`<adapter>[:profile]`) through the
1076	    // merged view (split → parent lookup → overlay), exactly like shell spawn.
1077	    let adapters_dir = spt_store::perch::adapters_dir();
1078	    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
1079	        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
1080	        Ok(_) => {
1081	            eprintln!(
1082	                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
1083	                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
1084	            );
1085	            return 1;
1086	        }
1087	        Err(e) => {
1088	            eprintln!(
1089	                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
1090	                 harness adapter / valid profile on this node ({e}; spt adapter list)"
1091	            );
1092	            return 1;
1093	        }
1094	    };
1095
1096	    // A fresh bringup mints a provisional spawn-time session id; --resume carries
1097	    // a prior one (the harness reports the real id post-spawn either way).
1098	    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);
1099
1100	    if let Err(e) = spt_daemon::daemon::ensure_running() {
1101	        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
1102	        return 1;
1103	    }
1104	    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
1105	    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
1106	    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
1107	    // session history, and digest source relative to cwd. A failure to read the
1108	    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
1109	    let project_cwd = std::env::current_dir()
1110	        .ok()
1111	        .map(|p| p.to_string_lossy().into_owned());
1112	    match spt_daemon::harnesshost::launch_harness_brokered_in(
1113	        &spt_daemon::endpoint::broker_socket_name(),
1114	        id,
1115	        adapter,
1116	        &session_id,
1117	        &manifest,
1118	        project_cwd.as_deref(),
1119	    ) {
1120	        Ok(spawned) => {
1121	            eprintln!(
1122	                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
1123	                 (harness binds its perch on startup)",
1124	                spawned.pid
1125	            );
1126	        }
1127	        Err(e) => {
1128	            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
1129	            return 1;
1130	        }
1131	    }
1132
1133	    // Terminal action: start (return now) | view (read-only attach) | attach
1134	    // (default). Attach/view route through the SAME rc pump as `spt rc`.
1135	    if start {
1136	        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
1137	        return 0;
1138	    }
1139	    match crate::rc::run_attach(id, view) {
1140	        Ok(()) => 0,
1141	        Err(e) => {
1142	            eprintln!("RC_FAIL:{id}: {e}");
1143	            1
1144	        }
1145	    }
1146	}
1147
1148	/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move
1149	/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
1150	/// runs before anything mutates, so a refused fork leaves no half-fork.
1151	///
1152	/// 1. charset + same-id guard (a node holds one perch per name — the
1153	///    same-name fork is the M5 remote arm);
1154	/// 2. target membership (the fork's home must be a subnet we hold);
1155	/// 3. source exists locally (the remote-source arm is M5);
1156	/// 4. join-time collision in the TARGET subnet (the snapshot's rows — any
1157	///    node, any status: a resting holder still owns its name) and locally;
1158	/// 5. context fork (one-time copy, copied-then-independent) + perch mint
1159	///    (home = target, adapter/blurb carried — the fork starts as the same
1160	///    kind of agent) + sync scope seeded [target];
1161	/// 6. `--delete-source` LAST: the source vanishes only after the fork is
1162	///    whole (perch + tracked mind + address/ready marker).
1163	// [impl->REQ-INST-15]
1164	fn cmd_fork(src: &str, new_id: &str, subnet: &str, delete_source: bool) -> i32 {
1165	    use spt_store::contextstore::ContextStore;
1166
1167	    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {
1168	        eprintln!("ERROR: invalid new id: {e}");
1169	        return 2;
1170	    }
1171	    if src == new_id {
1172	        eprintln!(
1173	            "SAME_ID:{src} — a same-node fork needs a new id (one perch per name per node); \

/// `spt endpoint run` brings up an spt-hosted harness endpoint (M12-W1).
/// Resolves the harness adapter, spawns its `[session.self]` into a broker-held
/// PTY (the harness self-registers its perch on bind), then starts, attaches, or
/// views per the chosen terminal-action flag.
// [impl->REQ-HOST-RUN-1]
fn cmd_endpoint_run(
    adapter: &str,
    id: &str,
    resume: Option<String>,
    start: bool,
    // `--attach` is the default terminal action; the bool is accepted for flag
    // symmetry but `start`/`view` are what branch (attach = neither).
    _attach: bool,
    view: bool,
) -> i32 {
    // id charset (the harness binds to exactly this id via argv).
    // [impl->REQ-HAZARD-ID-CHARSET]
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return 2;
    }
    // Resolve the harness adapter option (`<adapter>[:profile]`) through the
    // merged view (split → parent lookup → overlay), exactly like shell spawn.
    let adapters_dir = spt_store::perch::adapters_dir();
    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
        Ok(_) => {
            eprintln!(
                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
            );
            return 1;
        }
        Err(e) => {
            eprintln!(
                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
                 harness adapter / valid profile on this node ({e}; spt adapter list)"
            );
            return 1;
        }
    };

    // A fresh bringup mints a provisional spawn-time session id; --resume carries
    // a prior one (the harness reports the real id post-spawn either way).
    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);

    if let Err(e) = spt_daemon::daemon::ensure_running() {
        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
        return 1;
    }
    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
    // session history, and digest source relative to cwd. A failure to read the
    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
    let project_cwd = std::env::current_dir()
        .ok()
        .map(|p| p.to_string_lossy().into_owned());
    match spt_daemon::harnesshost::launch_harness_brokered_in(
        &spt_daemon::endpoint::broker_socket_name(),
        id,
        adapter,
        &session_id,
        &manifest,
        project_cwd.as_deref(),
    ) {
        Ok(spawned) => {
            eprintln!(
                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
                 (harness binds its perch on startup)",
                spawned.pid
            );
        }
        Err(e) => {
            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
            return 1;
        }
    }

    // Terminal action: start (return now) | view (read-only attach) | attach
    // (default). Attach/view route through the SAME rc pump as `spt rc`.
    if start {
        eprintln!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)");
        return 0;
    }
    match crate::rc::run_attach(id, view) {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("RC_FAIL:{id}: {e}");
            1
        }
    }
}

/// `spt fork <src> <new_id> --subnet <target>` — the cross-subnet move
/// (REQ-INST-15, ADR-0010). Order of operations is refusal-first: every gate
/// runs before anything mutates, so a refused fork leaves no half-fork.
///
/// 1. charset + same-id guard (a node holds one perch per name — the
///    same-name fork is the M5 remote arm);
/// 2. target membership (the fork's home must be a subnet we hold);
/// 3. source exists locally (the remote-source arm is M5);
/// 4. join-time collision in the TARGET subnet (the snapshot's rows — any
///    node, any status: a resting holder still owns its name) and locally;
/// 5. context fork (one-time copy, copied-then-independent) + perch mint
///    (home = target, adapter/blurb carried — the fork starts as the same
///    kind of agent) + sync scope seeded [target];
/// 6. `--delete-source` LAST: the source vanishes only after the fork is
///    whole (perch + tracked mind + address/ready marker).
// [impl->REQ-INST-15]
fn cmd_fork(src: &str, new_id: &str, subnet: &str, delete_source: bool) -> i32 {
    use spt_store::contextstore::ContextStore;

    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {
        eprintln!("ERROR: invalid new id: {e}");
        return 2;
    }
    if src == new_id {
        eprintln!(
            "SAME_ID:{src} — a same-node fork needs a new id (one perch per name per node); \


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


1412	fn cmd_endpoint_list(local: bool, subnet: Option<String>, detail: bool) -> i32 {
1413	    use spt_net::net::registry::resource_projection;
1414	    use spt_store::visibility::VisibilityStore;
1415
1416	    if local {
1417	        return cmd_list_local();
1418	    }
1419	    let subnets = SubnetStore::load();
1420	    if let Some(n) = &subnet {
1421	        if subnets.find(n).is_none() {
1422	            eprintln!("NO_SUCH_SUBNET:{n}");
1423	            return 2;
1424	        }
1425	    }
1426
1427	    // SELF pin (decision 2): the session's own endpoint renders first and
1428	    // distinctly, whatever subnet groups follow.
1429	    if let Some(self_id) = roster::detect_self_id() {
1430	        let state = roster::enumerate()
1431	            .into_iter()
1432	            .find(|p| p.id == self_id)
1433	            .map(|p| format!("{}  ready={} alive={}", p.state, p.ready, p.alive))
1434	            .unwrap_or_else(|| "(no local perch)".to_string());
1435	        println!("SELF: {self_id}  {state}");
1436	    }
1437
1438	    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
1439	    let vis = VisibilityStore::load();
1440	    let names: Vec<String> = match subnet {
1441	        Some(one) => vec![one],
1442	        None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),
1443	    };
1444	    if names.is_empty() {
1445	        eprintln!("(no subnets — `spt endpoint list --local` lists this node's perches)");
1446	        return 0;
1447	    }
1448	    let mut shown = 0usize;
1449	    for name in names {
1450	        println!("SUBNET {name}");
1451	        let Some(reg) = regs.get(&name) else {
1452	            println!("  (no gossip yet)");
1453	            continue;
1454	        };
1455	        let hide_new = subnets
1456	            .find(&name)
1457	            .map(|s| s.hide_new_endpoints)
1458	            .unwrap_or(false);
1459	        let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));
1460	        if rows.is_empty() {
1461	            println!("  (no endpoints)");
1462	            continue;
1463	        }
1464	        for row in rows {
1465	            let node = node_display(&row.node, None, false);
1466	            if detail {
1467	                println!(
1468	                    "  {}\t{}\t{:?}\t{}",
1469	                    row.endpoint_id,
1470	                    node,
1471	                    row.status,
1472	                    row.resources.as_deref().unwrap_or("-")
1473	                );
1474	            } else {
1475	                println!("  {}\t{}\t{:?}", row.endpoint_id, node, row.status);
1476	            }
1477	            shown += 1;
1478	        }
1479	    }
1480	    eprintln!("ENDPOINTS:{shown}");
1481	    0
1482	}
1483
1484	/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges
1485	/// (REQ-INST-3): apply the event against the local endpoint's durable record
1486	/// through the daemon-side transition host (echo gate-armed for the
1487	/// endpoint's own pulse loop; wake fires the boundary resurface + freshness
1488	/// pull). A qualified `[subnet:]id@node` rides the remote rest arm (M5-D5b,
1489	/// REQ-INST-6): resolve the peer from the registry snapshots, dial, and let
1490	/// the TARGET node's transition host run the edge — exit codes mirror local
1491	/// (edge / NO_EDGE = 0, refusal or failure = 1).
1492	// [impl->REQ-INST-3]
1493	// [impl->REQ-INST-6]
1494	fn cmd_rest(id: &str, event: spt_daemon::RestEvent, verb: &str) -> i32 {
1495	    if id.contains('@') || id.contains(':') {
1496	        let tag = match event {
1497	            spt_daemon::RestEvent::Suspend => spt_net::net::rest::REST_EVENT_SUSPEND,
1498	            spt_daemon::RestEvent::Wake => spt_net::net::rest::REST_EVENT_WAKE,
1499	            // Only the manual verbs route here; the other events are
1500	            // daemon-internal feeds with no CLI surface.
1501	            _ => {
1502	                eprintln!("{verb}_FAIL:{id}: event has no remote arm");
1503	                return 1;
1504	            }
1505	        };
1506	        return match crate::wansend::wan_rest(id, tag) {
1507	            crate::wansend::WanRestOutcome::Edge { node, detail } => {
1508	                eprintln!("{verb}:{id} ({detail}) at {node}");
1509	                0
1510	            }
1511	            crate::wansend::WanRestOutcome::NoEdge { node } => {
1512	                eprintln!("NO_EDGE:{id} (already in the target state) at {node}");
1513	                0
1514	            }
1515	            crate::wansend::WanRestOutcome::NoReply { node } => {
1516	                eprintln!(
1517	                    "{verb}_REFUSED:{id}: {node} closed the stream without a reply (access gate)"
1518	                );
1519	                1
1520	            }
1521	            crate::wansend::WanRestOutcome::RemoteFail { node, detail } => {
1522	                eprintln!("{verb}_FAIL:{id} at {node}: {detail}");
1523	                1
1524	            }
1525	            crate::wansend::WanRestOutcome::Ambiguous(a) => {
1526	                eprintln!("AMBIGUOUS:{id} — {a}");
1527	                1
1528	            }
1529	            crate::wansend::WanRestOutcome::NotFound => {
1530	                eprintln!("NO_ENDPOINT:{id}: no routable remote instance in any visible subnet");
1531	                1
1532	            }
1533	            crate::wansend::WanRestOutcome::Failed(e) => {
1534	                eprintln!("{verb}_FAIL:{id}: {e}");
1535	                1
1536	            }
1537	        };
1538	    }
1539	    let knob = spt_daemon::DaemonConfig::load().auto_suspend_after_ms;
1540	    match spt_daemon::daemon_rest_event(id, event, knob) {
1541	        Ok(Some(report)) => {
1542	            eprintln!("{verb}:{id} ({:?} -> {:?})", report.from, report.to);
1543	            0
1544	        }
1545	        Ok(None) => {
1546	            // Idempotent UX: the machine reports no edge (already there).
1547	            eprintln!("NO_EDGE:{id} (already in the target state)");
1548	            0
1549	        }
1550	        Err(e) => {
1551	            eprintln!("{verb}_FAIL:{id}: {e}");
1552	            1
1553	        }
1554	    }
1555	}
1556
1557	/// `spt shutdown [id]` — the agent's graceful self-suspend verb (M5-D4d,
1558	/// CONTEXT §Shell model: "an agent gracefully shuts down *its own* endpoint
1559	/// → suspended; fires the transition echo-commune; cascades its persistent
1560	/// shells offline"). Two legs through existing machinery: (1) soft-stop the
1561	/// listener (drop the ready marker — the session-over half `spt suspend`
1562	/// does not do); (2) the suspend edge via the daemon-side transition host —
1563	/// which arms the echo gate and runs the D4a shell cascade. Idempotent like
1564	/// every rest verb.
1565	// [impl->REQ-SHELL-2]
1566	fn cmd_shutdown(id: Option<String>) -> i32 {
1567	    let Some(id) = id.or_else(roster::detect_self_id) else {
1568	        eprintln!(
1569	            "NO_SELF: shutdown needs an endpoint (spt endpoint shutdown <id> or $OWL_SESSION_ID)"
1570	        );
1571	        return 1;

fn cmd_endpoint_list(local: bool, subnet: Option<String>, detail: bool) -> i32 {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    if local {
        return cmd_list_local();
    }
    let subnets = SubnetStore::load();
    if let Some(n) = &subnet {
        if subnets.find(n).is_none() {
            eprintln!("NO_SUCH_SUBNET:{n}");
            return 2;
        }
    }

    // SELF pin (decision 2): the session's own endpoint renders first and
    // distinctly, whatever subnet groups follow.
    if let Some(self_id) = roster::detect_self_id() {
        let state = roster::enumerate()
            .into_iter()
            .find(|p| p.id == self_id)
            .map(|p| format!("{}  ready={} alive={}", p.state, p.ready, p.alive))
            .unwrap_or_else(|| "(no local perch)".to_string());
        println!("SELF: {self_id}  {state}");
    }

    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let names: Vec<String> = match subnet {
        Some(one) => vec![one],
        None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),
    };
    if names.is_empty() {
        eprintln!("(no subnets — `spt endpoint list --local` lists this node's perches)");
        return 0;
    }
    let mut shown = 0usize;
    for name in names {
        println!("SUBNET {name}");
        let Some(reg) = regs.get(&name) else {
            println!("  (no gossip yet)");
            continue;
        };
        let hide_new = subnets
            .find(&name)
            .map(|s| s.hide_new_endpoints)
            .unwrap_or(false);
        let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));
        if rows.is_empty() {
            println!("  (no endpoints)");
            continue;
        }
        for row in rows {
            let node = node_display(&row.node, None, false);
            if detail {
                println!(
                    "  {}\t{}\t{:?}\t{}",
                    row.endpoint_id,
                    node,
                    row.status,
                    row.resources.as_deref().unwrap_or("-")
                );
            } else {
                println!("  {}\t{}\t{:?}", row.endpoint_id, node, row.status);
            }
            shown += 1;
        }
    }
    eprintln!("ENDPOINTS:{shown}");
    0
}

/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges
/// (REQ-INST-3): apply the event against the local endpoint's durable record
/// through the daemon-side transition host (echo gate-armed for the
/// endpoint's own pulse loop; wake fires the boundary resurface + freshness
/// pull). A qualified `[subnet:]id@node` rides the remote rest arm (M5-D5b,
/// REQ-INST-6): resolve the peer from the registry snapshots, dial, and let
/// the TARGET node's transition host run the edge — exit codes mirror local
/// (edge / NO_EDGE = 0, refusal or failure = 1).
// [impl->REQ-INST-3]
// [impl->REQ-INST-6]
fn cmd_rest(id: &str, event: spt_daemon::RestEvent, verb: &str) -> i32 {
    if id.contains('@') || id.contains(':') {
        let tag = match event {
            spt_daemon::RestEvent::Suspend => spt_net::net::rest::REST_EVENT_SUSPEND,
            spt_daemon::RestEvent::Wake => spt_net::net::rest::REST_EVENT_WAKE,
            // Only the manual verbs route here; the other events are
            // daemon-internal feeds with no CLI surface.
            _ => {
                eprintln!("{verb}_FAIL:{id}: event has no remote arm");
                return 1;
            }
        };
        return match crate::wansend::wan_rest(id, tag) {
            crate::wansend::WanRestOutcome::Edge { node, detail } => {
                eprintln!("{verb}:{id} ({detail}) at {node}");
                0
            }
            crate::wansend::WanRestOutcome::NoEdge { node } => {
                eprintln!("NO_EDGE:{id} (already in the target state) at {node}");
                0
            }
            crate::wansend::WanRestOutcome::NoReply { node } => {
                eprintln!(
                    "{verb}_REFUSED:{id}: {node} closed the stream without a reply (access gate)"
                );
                1
            }
            crate::wansend::WanRestOutcome::RemoteFail { node, detail } => {
                eprintln!("{verb}_FAIL:{id} at {node}: {detail}");
                1
            }
            crate::wansend::WanRestOutcome::Ambiguous(a) => {
                eprintln!("AMBIGUOUS:{id} — {a}");
                1
            }
            crate::wansend::WanRestOutcome::NotFound => {
                eprintln!("NO_ENDPOINT:{id}: no routable remote instance in any visible subnet");
                1
            }
            crate::wansend::WanRestOutcome::Failed(e) => {
                eprintln!("{verb}_FAIL:{id}: {e}");
                1
            }
        };
    }
    let knob = spt_daemon::DaemonConfig::load().auto_suspend_after_ms;
    match spt_daemon::daemon_rest_event(id, event, knob) {
        Ok(Some(report)) => {
            eprintln!("{verb}:{id} ({:?} -> {:?})", report.from, report.to);
            0
        }
        Ok(None) => {
            // Idempotent UX: the machine reports no edge (already there).
            eprintln!("NO_EDGE:{id} (already in the target state)");
            0
        }
        Err(e) => {
            eprintln!("{verb}_FAIL:{id}: {e}");
            1
        }
    }
}

/// `spt shutdown [id]` — the agent's graceful self-suspend verb (M5-D4d,
/// CONTEXT §Shell model: "an agent gracefully shuts down *its own* endpoint
/// → suspended; fires the transition echo-commune; cascades its persistent
/// shells offline"). Two legs through existing machinery: (1) soft-stop the
/// listener (drop the ready marker — the session-over half `spt suspend`
/// does not do); (2) the suspend edge via the daemon-side transition host —
/// which arms the echo gate and runs the D4a shell cascade. Idempotent like
/// every rest verb.
// [impl->REQ-SHELL-2]
fn cmd_shutdown(id: Option<String>) -> i32 {
    let Some(id) = id.or_else(roster::detect_self_id) else {
        eprintln!(
            "NO_SELF: shutdown needs an endpoint (spt endpoint shutdown <id> or $OWL_SESSION_ID)"
        );
        return 1;


Check spt Cargo.toml deps + rc.rs terminal handling + endpoint description/roster surfaces.


1	[package]
2	name = "spt"
3	version.workspace = true
4	edition = "2021"
5	publish = false
6	description = "spt-core CLI: the single `spt` binary. Thin command layer over spt-msg (send/ring/ready/list/stop/whoami) + the harness-contract `api` surface (spt-runtime)."
7
8	[[bin]]
9	name = "spt"
10	path = "src/main.rs"
11
12	[dependencies]
13	spt-proto = { path = "../spt-proto" }
14	spt-store = { path = "../spt-store" }
15	spt-msg = { path = "../spt-msg" }
16	spt-runtime = { path = "../spt-runtime" }
17	spt-live = { path = "../spt-live" }
18	spt-daemon = { path = "../spt-daemon" }
19	# The `api digest-entry` push door validates the inbound record against the
20	# published digest-record contract (`spt_term::record_to_tagged`) — REQ-TERM-4.
21	spt-term = { path = "../spt-term" }
22	# The reference binary ships the WAN transport on by default (ADR-0002). D2g reuses its
23	# RFC-6238 TotpSeed (spt_net::net::pairing::totp) to render a subnet's local pairing
24	# code — reused, not reimplemented, so the CLI code matches the ceremony's exactly.
25	spt-net = { path = "../spt-net" }
26	# `wrap_help`: wrap long help text to the terminal width with a hanging indent
27	# (without it clap emits unwrapped lines and the terminal hard-wraps to col 0).
28	clap = { version = "4", features = ["derive", "wrap_help"] }
29	# `spt rc` raw terminal mode for the attach pump (M12-W1): enter/leave raw mode
30	# so keystrokes pass through to the broker-held PTY unbuffered + unechoed. The
31	# same crate W2's ratatui picker builds on, so it lands once for the milestone.
32	crossterm = "0.28"
33	# The WAN send leg (D9-1-4) serializes the dial address + reads registry
34	# snapshots - same pinned shape as spt-daemon's.
35	serde_json = { version = "1", features = ["preserve_order"] }
36	# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
37	# otpauth:// URI as unicode half-blocks. default-features off drops the
38	# image/svg renderers — the unicode renderer is all the CLI needs.
39	qrcode = { version = "0.14", default-features = false }
40	# Local-timezone rendering of the pairing code's expiry instant (std has no
41	# local tz). Clock feature only — no serde, no wasm.
42	chrono = { version = "0.4", default-features = false, features = ["clock"] }
43	# `spt update fetch` (REQ-UPD-7): HTTPS GET of the signed release from the
44	# GitHub origin. Blocking client + rustls (no OpenSSL); already in the tree
45	# transitively (iroh), so no new vendor. The fetch runs in the CLI, NOT the
46	# daemon — the daemon keeps zero HTTP surface (iroh QUIC only).
47	reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] }
48
49	# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the
50	# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing
51	# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.
52	[target.'cfg(unix)'.dependencies]
53	libc = "0.2"
54
55	[target.'cfg(windows)'.dependencies]
56	windows-sys = { version = "0.59", features = [
57	    "Win32_Foundation",
58	    "Win32_Security",
59	    "Win32_System_Threading",
60	] }
61
62	[dev-dependencies]
63	tempfile = "3"
64	toml = "0.8"
65	# install_e2e stages the built binary as a release asset (SHA256SUMS) for the
66	# D2 installer-script rung — same sha2 the workspace already carries.
67	sha2 = "0.10"
68	# The D7-1 process-level survival int-test (brain_survive.rs) drives the
69	# production supervisor + an in-process broker/NetHost while spawning the REAL
70	# `spt daemon brain` child (CARGO_BIN_EXE_spt). Integration tests don't inherit
71	# the lib's normal deps, so the daemon/net/identity surfaces it touches directly
72	# are re-declared here (the spt-daemon dev-dep pattern).
73	spt-daemon = { path = "../spt-daemon" }
74	spt-net = { path = "../spt-net" }
75	spt-proto = { path = "../spt-proto" }
76

[package]
name = "spt"
version.workspace = true
edition = "2021"
publish = false
description = "spt-core CLI: the single `spt` binary. Thin command layer over spt-msg (send/ring/ready/list/stop/whoami) + the harness-contract `api` surface (spt-runtime)."

[[bin]]
name = "spt"
path = "src/main.rs"

[dependencies]
spt-proto = { path = "../spt-proto" }
spt-store = { path = "../spt-store" }
spt-msg = { path = "../spt-msg" }
spt-runtime = { path = "../spt-runtime" }
spt-live = { path = "../spt-live" }
spt-daemon = { path = "../spt-daemon" }
# The `api digest-entry` push door validates the inbound record against the
# published digest-record contract (`spt_term::record_to_tagged`) — REQ-TERM-4.
spt-term = { path = "../spt-term" }
# The reference binary ships the WAN transport on by default (ADR-0002). D2g reuses its
# RFC-6238 TotpSeed (spt_net::net::pairing::totp) to render a subnet's local pairing
# code — reused, not reimplemented, so the CLI code matches the ceremony's exactly.
spt-net = { path = "../spt-net" }
# `wrap_help`: wrap long help text to the terminal width with a hanging indent
# (without it clap emits unwrapped lines and the terminal hard-wraps to col 0).
clap = { version = "4", features = ["derive", "wrap_help"] }
# `spt rc` raw terminal mode for the attach pump (M12-W1): enter/leave raw mode
# so keystrokes pass through to the broker-held PTY unbuffered + unechoed. The
# same crate W2's ratatui picker builds on, so it lands once for the milestone.
crossterm = "0.28"
# The WAN send leg (D9-1-4) serializes the dial address + reads registry
# snapshots - same pinned shape as spt-daemon's.
serde_json = { version = "1", features = ["preserve_order"] }
# Terminal QR render for `spt subnet create` (M7 plan decision 11): the
# otpauth:// URI as unicode half-blocks. default-features off drops the
# image/svg renderers — the unicode renderer is all the CLI needs.
qrcode = { version = "0.14", default-features = false }
# Local-timezone rendering of the pairing code's expiry instant (std has no
# local tz). Clock feature only — no serde, no wasm.
chrono = { version = "0.4", default-features = false, features = ["clock"] }
# `spt update fetch` (REQ-UPD-7): HTTPS GET of the signed release from the
# GitHub origin. Blocking client + rustls (no OpenSSL); already in the tree
# transitively (iroh), so no new vendor. The fetch runs in the CLI, NOT the
# daemon — the daemon keeps zero HTTP surface (iroh QUIC only).
reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] }

# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the
# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing
# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
    "Win32_Foundation",
    "Win32_Security",
    "Win32_System_Threading",
] }

[dev-dependencies]
tempfile = "3"
toml = "0.8"
# install_e2e stages the built binary as a release asset (SHA256SUMS) for the
# D2 installer-script rung — same sha2 the workspace already carries.
sha2 = "0.10"
# The D7-1 process-level survival int-test (brain_survive.rs) drives the
# production supervisor + an in-process broker/NetHost while spawning the REAL
# `spt daemon brain` child (CARGO_BIN_EXE_spt). Integration tests don't inherit
# the lib's normal deps, so the daemon/net/identity surfaces it touches directly
# are re-declared here (the spt-daemon dev-dep pattern).
spt-daemon = { path = "../spt-daemon" }
spt-net = { path = "../spt-net" }
spt-proto = { path = "../spt-proto" }


360:    Description {
362:        action: Option<DescriptionCmd>,
380:/// `spt endpoint description <action>` subcommands (M8 decision 25c — the
384:enum DescriptionCmd {
947:            EndpointCmd::Description { action } => {
948:                cmd_description(action.unwrap_or(DescriptionCmd::Show { id: None }))
1272:fn cmd_description(action: DescriptionCmd) -> i32 {
1285:        DescriptionCmd::Set { text, id } => {
1308:        DescriptionCmd::Show { id } => {
1883:/// attached to").
3590:                "attached"
6196:                action: Some(EndpointCmd::Description {
6197:                    action: Some(DescriptionCmd::Set { .. })
6204:                action: Some(EndpointCmd::Description { action: None })
8618:            ("BIGNET".to_string(), "attached"),
8626:        assert!(out.contains("  BIGNET: attached\n"));


1272	fn cmd_description(action: DescriptionCmd) -> i32 {
1273	    /// Resolve the operated endpoint: explicit `--id`, else session detect.
1274	    fn target(id: Option<String>) -> Result<String, i32> {
1275	        match id.or_else(roster::detect_self_id) {
1276	            Some(id) => Ok(id),
1277	            None => {
1278	                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");
1279	                Err(1)
1280	            }
1281	        }
1282	    }
1283
1284	    match action {
1285	        DescriptionCmd::Set { text, id } => {
1286	            let id = match target(id) {
1287	                Ok(id) => id,
1288	                Err(code) => return code,
1289	            };
1290	            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
1291	            let blurb = (!text.trim().is_empty()).then_some(text.trim());
1292	            match spt_store::info::set_resources(&perch_path, blurb) {
1293	                Ok(()) => {
1294	                    match blurb {
1295	                        Some(b) => eprintln!("DESCRIPTION_SET:{id}: {b}"),
1296	                        None => {
1297	                            eprintln!("DESCRIPTION_CLEARED:{id} (node seed advertises, if any)")
1298	                        }
1299	                    }
1300	                    0
1301	                }
1302	                Err(e) => {
1303	                    eprintln!("DESCRIPTION_FAIL:{id}: {e}");
1304	                    1
1305	                }
1306	            }
1307	        }
1308	        DescriptionCmd::Show { id } => {
1309	            let id = match target(id) {
1310	                Ok(id) => id,
1311	                Err(code) => return code,
1312	            };
1313	            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
1314	            match spt_store::info::read_info(&perch_path) {
1315	                Some(rec) => {
1316	                    match rec.resources {
1317	                        Some(b) => println!("{b}"),
1318	                        None => println!("(no authored blurb — the node seed advertises, if any)"),
1319	                    }
1320	                    0
1321	                }
1322	                None => {
1323	                    eprintln!("NO_ENDPOINT:{id}");
1324	                    1
1325	                }
1326	            }
1327	        }
1328	    }
1329	}
1330
1331	/// `spt endpoint role [--overwrite <file>]` — the **sole writer** of the durable

fn cmd_description(action: DescriptionCmd) -> i32 {
    /// Resolve the operated endpoint: explicit `--id`, else session detect.
    fn target(id: Option<String>) -> Result<String, i32> {
        match id.or_else(roster::detect_self_id) {
            Some(id) => Ok(id),
            None => {
                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");
                Err(1)
            }
        }
    }

    match action {
        DescriptionCmd::Set { text, id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            let blurb = (!text.trim().is_empty()).then_some(text.trim());
            match spt_store::info::set_resources(&perch_path, blurb) {
                Ok(()) => {
                    match blurb {
                        Some(b) => eprintln!("DESCRIPTION_SET:{id}: {b}"),
                        None => {
                            eprintln!("DESCRIPTION_CLEARED:{id} (node seed advertises, if any)")
                        }
                    }
                    0
                }
                Err(e) => {
                    eprintln!("DESCRIPTION_FAIL:{id}: {e}");
                    1
                }
            }
        }
        DescriptionCmd::Show { id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            match spt_store::info::read_info(&perch_path) {
                Some(rec) => {
                    match rec.resources {
                        Some(b) => println!("{b}"),
                        None => println!("(no authored blurb — the node seed advertises, if any)"),
                    }
                    0
                }
                None => {
                    eprintln!("NO_ENDPOINT:{id}");
                    1
                }
            }
        }
    }
}

/// `spt endpoint role [--overwrite <file>]` — the **sole writer** of the durable


Grep

project history|project_history|session log|session_log|projects_for|history list|resume.*session|prior session


Found 44 files
CONTEXT.md
traceable-reqs.toml
crates\spt-daemon\tests\resume.rs
crates\spt\src\cli.rs
crates\spt-daemon\src\harnesshost.rs
crates\spt-daemon\src\broker.rs
crates\spt\src\rc.rs
crates\spt-daemon\src\brain.rs
M12-W1-PLAN.md
M12-PLAN.md
M12-ENDPOINT-RUN-PICKER.md
CHANGELOG.md
docs\adr\0008-live-activity-buffer-pty-digest.md
docs\DEFERRED.md
docs\MANIFEST.md
docs-site\src\harness-contract\manifest.md
docs-site\src\cli\reference.md
crates\spt\src\api\reporting.rs
crates\spt-term\src\lib.rs
crates\spt-term\src\projection.rs
crates\spt-live\src\resume.rs
crates\spt-live\src\digest.rs
crates\spt-live\src\echo.rs
crates\spt-daemon\tests\digest.rs
crates\spt-daemon\tests\two_origin_spanning.rs
crates\spt-daemon\src\digest.rs
ROADMAP.md
docs-site\src\terminal\overview.md
M9-WAVE3-JIT.md
M9-PLAN.md
docs\KNOWN-HAZARDS.md
crates\spt-daemon\src\brainproc.rs
RESTORATION-D7-PLAN.md
RESTORATION-D6-PLAN.md
RESTORATION-D5-PLAN.md
RESTORATION-D4-PLAN.md
RESTORATION-PLAN.md
RESTORATION-D3-PLAN.md
crates\spt-live\src\turn.rs
crates\spt-live\src\signoff.rs
PRD.md
M2a-PLAN.md
M2b-PLAN.md
docs\CONTEXT-MEMORY.md


Few more checks to sharpen design questions: adapter/profile enumeration, attach-liveness, roster surface.


fn cmd_adapter_list|fn list_adapters|profiles_dir|fn .*profiles|list_profiles|registered_adapters|fn enumerate|kind == .*Harness|AdapterKind::Harness


adapters\mock\tests\manifest_valid.rs:17:    assert_eq!(m.adapter.kind, AdapterKind::Harness);
crates\spt\src\roster.rs:25:pub fn enumerate() -> Vec<PerchEntry> {
crates\spt\src\cli.rs:1079:        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
crates\spt-runtime\src\manifest.rs:662:            AdapterKind::Harness if self.shell.is_some() => {
crates\spt-runtime\src\manifest.rs:833:        assert_eq!(m.adapter.kind, AdapterKind::Harness);
crates\spt-runtime\src\registry.rs:421:fn local_profiles_dir(adapters_dir: &Path, adapter: &str) -> PathBuf {
crates\spt-runtime\src\registry.rs:427:    local_profiles_dir(adapters_dir, adapter).join(format!("{profile}.toml"))
crates\spt-runtime\src\registry.rs:463:    let Ok(entries) = std::fs::read_dir(local_profiles_dir(adapters_dir, adapter)) else {
crates\spt-runtime\src\registry.rs:500:    let dir = local_profiles_dir(adapters_dir, adapter);
crates\spt-runtime\src\registry.rs:553:            local_profiles_dir(adapters_dir, adapter)
crates\spt-runtime\src\registry.rs:774:        assert_eq!((h.mode, h.kind), (RecordMode::Copy, AdapterKind::Harness));
crates\spt-daemon\src\harnesshost.rs:70:    if manifest.adapter.kind != AdapterKind::Harness {


Found 19 files
crates\spt-daemon\src\applyhost.rs
crates\spt-daemon\src\broker.rs
crates\spt-daemon\src\msg.rs
crates\spt-daemon\src\nethost.rs
crates\spt-daemon\src\brain.rs
crates\spt-daemon\src\lib.rs
crates\spt-daemon\src\resting.rs
crates\spt-daemon\src\presence.rs
crates\spt-daemon\src\notif.rs
crates\spt-daemon\src\dispatch.rs
crates\spt-daemon\src\brainproc.rs
crates\spt-daemon\src\serveprobe.rs
crates\spt-daemon\src\daemon.rs
crates\spt-daemon\src\endpoint.rs
crates\spt-daemon\src\deelevate.rs
crates\spt-daemon\src\grants.rs
crates\spt-daemon\src\config.rs
crates\spt-daemon\src\consent.rs
crates\spt-daemon\src\attach.rs


Enough to frame. Picker = big wave, multiple data-model gaps (attached-status query, project history, session log, kick-op) → design-check warranted, matches W1.5/W3 pattern. Writing JIT plan with investigation TODOs + open questions for doyle.


Write

# M12 Wave 2 — `spt endpoint run` interactive picker (ratatui) — JIT plan

> todlando 2026-06-14, post W1.5 + W3 gate (both PASS, committed 0c687fe / c02aabb). W2 is
> the BIGGEST remaining wave and the user-facing face of M12. **DESIGN-CHECK PENDING with
> doyle before building** — open questions below center on data surfaces the picker needs
> that may not exist yet (attached-state query, project history, per-endpoint session log,
> kick-and-attach op). Build only after his ruling. Full spec: `M12-ENDPOINT-RUN-PICKER.md`;
> wave scope: `M12-PLAN.md` Wave 2.

## Scope (M12-PLAN.md Wave 2 / picker spec)
- **T2.1** Layer 1 (kind: Create new | Pick existing) + create-new branch: choose harness
  adapter (profiles tree-nested under each adapter) → enter id (charset-validated) → start.
- **T2.2** Pick-existing: category L/R (`<project> | Local node | Subnet`), grouped +
  alpha-sorted, status squares (online green ■ / offline gray ▢ / attached blue ■),
  type-to-filter (`/`), pinned keybind legend, **two-pane right-half description**
  (adapter:profile · project history newest→oldest · `endpoint description`).
- **T2.3** Confirm layer + status-dependent options: Attach/Start/View · **Kick <node> and
  attach** (attached-only) · Instantiate-locally (remote) · Change-adapter (offline) · Fork ·
  **Resume-from-history** (offline-only; per-endpoint session log + manifest `resume`; titles
  `<project> @ <ts> (...id5)`).
- **T2.4** `s` keybind = build/update `cc-<id>` shortcut at project root with baked
  non-interactive flags. Requires the flag set to cover every terminal action of the flow.
- **Phone caveat:** don't hard-couple interaction to keybinds (future tap-mode layer can
  reuse the same state model + action enum).

## What EXISTS today (investigation 2026-06-14)
- **Terminal action core (the picker terminates INTO this):** `cli.rs:cmd_endpoint_run`
  (T1.1) — adapter resolve → `harnesshost::launch_harness_brokered_in` → start/attach/view
  via `rc::run_attach`. Picker = an interactive front-end that gathers (adapter, id,
  resume, action) then calls the same path. `Run{}` flags today: `--adapter <a[:profile]>`
  `--id` `--resume <session>` `--start|--attach|--view`. **No `--create` flag** (run mints
  fresh unless `--resume`); no kick/instantiate/fork-from-run flags.
- **Adapter + profile enumeration:** `registry::resolve_option` (split `<adapter>:<profile>`
  → parent lookup → overlay). Profiles live at `adapters/<adapter>/profiles/<profile>.toml`;
  `registry.rs:local_profiles_dir` + read_dir enumerate them (need a tidy "list harness
  adapters + their profiles" helper for L2; building blocks present). `AdapterKind::Harness`
  gates the harness-only list.
- **Endpoint discovery:** `roster::enumerate()` (local perches: state/ready/alive) +
  `roster::detect_self_id`; `wansend::load_snapshots(identity/registry)` per-subnet snapshots
  → `resource_projection(reg, hidden_fn)` = visible+routable rows `(endpoint_id, node, status,
  resources)`; `VisibilityStore` + `SubnetStore`. This is the L2 pick-existing data spine.
- **Status:** registry `Status{Active,Dormant,Offline,Suspended}` → online/offline mapping.
  **"Attached" (blue ■) has no obvious query** (see Q1).
- **Endpoint description:** `spt_store::info::read_info().resources` (the `endpoint
  description` blurb).
- **Deps:** `crossterm = "0.28"` already in `spt/Cargo.toml` (landed W1 for the rc raw-mode
  pump, explicitly noted "the same crate W2's ratatui picker builds on"). **ratatui +
  nucleo-matcher are NEW deps** to add this wave.
- **Reuse for terminal actions:** `cmd_fork` (Fork), `cmd_rest`/wake (resting), rc pump
  (attach/view) all exist as callable CLI fns.

## Investigation TODOs (read before coding — NOT yet done; gated on doyle scope ruling)
1. **`spt-daemon/src/attach.rs` + `presence.rs`**: is there a query "is endpoint E's
   broker PTY currently attached, and by which node/surface?" `serve_attach` serves a
   stream; PresenceLog tracks conn-level connect/disconnect. Determine whether attached-state
   (for the blue ■ + the "Kick <node>" option) is derivable from existing broker state or
   needs a new query op. (Drives Q1.)
2. **Project history surface**: where is a per-endpoint "projects worked, newest→oldest"
   list? Candidates: p-<project> context branches (contextstore), info.json, or the session
   log. The picker needs it for the description pane + confirm "Project history:". (Drives Q2.)
3. **Per-endpoint session log**: does a session-id log with last-message timestamps exist
   for Resume-from-history? `harnesshost::mint_session_id` mints; the adapter manifest
   `resume` declaration is the other half. Find where prior session ids are recorded (if at
   all) and what timestamp is available. (Drives Q3.)
4. **`vercel-labs/skills` `src/prompts/search-multiselect.ts`**: lift the state model
   `{query, cursor, selected:HashSet, locked:Vec}` + glyph set (`◆ ◇ ■ ● ○ ✓ • │ ─ ❯`) +
   windowed-cursor behavior. Map to ratatui `List`+`ListState`. (Look/feel parity.)
5. **`cmd_fork` / wake / change-adapter call shapes**: confirm the confirm-layer options can
   reuse the existing CLI fns (Fork, Instantiate-locally, Change-adapter, Resume) without
   new daemon plumbing beyond Q1/Q3 gaps.

## Open design questions for doyle
**Scope / data gaps (the load-bearing ones):**
1. **Attached-state (blue ■ + "Kick <node> and attach").** If no broker query exists for
   "who is attached to this PTY," this is the heaviest unknown. Options: **(a)** ship W2 with
   online/offline only, defer the blue tri-state + the Kick option to a follow-up once an
   attach-presence query exists; **(b)** build a broker attach-presence query op now (new
   daemon surface + the force-detach/kick wire op). Recommend **(a)** — keep W2 to the
   picker UX over surfaces that exist; the Kick path is a distinct broker capability with its
   own hazard surface (PTY ownership, KH zone) better scoped as its own slice. Confirm.
2. **Project history.** Is there an authoritative per-endpoint project list to read, or does
   W2 derive it from p-<project> context branches? If neither is cheap, propose rendering
   the description pane's history as "best-effort from contextstore, empty if none" and not
   blocking the wave on a new history store. Your call on the source.
3. **Resume-from-history.** Does a per-endpoint session-id log with timestamps exist? If not,
   Resume-from-history can't enumerate prior sessions honestly. Options: **(a)** defer
   Resume-from-history to when the session log exists (offline endpoints still get
   Attach/Start/View/Fork/Change-adapter); **(b)** establish a minimal per-endpoint session
   log now. Recommend **(a)** unless the log already exists.
4. **`cc-<id>` shortcut format (T2.4).** Cross-platform: a `.ps1` on Windows + a POSIX `sh`
   on Unix at project root, each baking the non-interactive `spt endpoint run` flags? Or a
   single portable form? Confirm the shape + the "Update if exists" overwrite semantics.
5. **Non-interactive flag completeness (T2.4 dependency).** The shortcut bakes the full
   selection. Today `Run{}` lacks `--create` (explicit create-vs-resume) and has no
   kick/instantiate/fork verbs. Proposal: add `--create` (explicit, the default-fresh made
   explicit) and let the shortcut cover only the *terminal* actions reachable
   non-interactively (adapter/profile + id + create|resume + start|attach|view); the
   interactive-only branches (Kick, Instantiate, Change-adapter, Fork) stay picker-only.
   Confirm the additive flag set.

**UX / structure (lower-stakes, my defaults unless you object):**
6. Single-binary in-process module `spt/src/picker/` (state model + ratatui render +
   key/action loop), invoked when `spt endpoint run` is called with no `--adapter`/`--id`
   (bare = interactive; flags present = today's non-interactive path, untouched). Keep the
   action enum the single source of truth so a future tap-mode layers on without touching it.
7. nucleo-matcher for `/` type-to-filter; ratatui two-column `Layout` for the description
   pane; keybind legend as a pinned bottom row. Esc=back, ←→=category, ↑↓=item, enter=confirm,
   /=filter, n=new, s=shortcut.

## Tentative tasks (pending doyle ruling — shapes WILL shift on Q1–Q5)
- **T2.0** Add REQs to `traceable-reqs.toml` FIRST (rule 3). Proposed: **REQ-RUN-PICKER**
  (interactive `spt endpoint run` TUI: kind layer, create-new adapter→profile→id, pick-existing
  category/group/status/filter/two-pane, confirm layer + status-dependent options) and
  **REQ-RUN-SHORTCUT** (`cc-<id>` generation + the additive non-interactive flag set).
  Granularity per doyle. Stages doc+impl+unit this wave (TUI int is harness-driven /
  manual — confirm whether `int` applies or stays a manual-verify like REQ-PAIR-6's OS probe).
- **T2.1** Add ratatui + nucleo-matcher deps. `spt/src/picker/` module: state model
  (`{query,cursor,selected,locked}` lifted) + the Layer-1 kind screen + create-new branch
  (harness-adapter list with profiles tree-nested → id-entry with live charset validation) →
  call `cmd_endpoint_run`. Glyph set + windowed cursor for feel parity.
- **T2.2** Pick-existing screen: category L/R over `[<cwd-project> | Local node | Subnet]`,
  grouping + alpha-sort per spec, status squares (scope per Q1), `/` filter (nucleo),
  pinned legend, two-pane description (adapter:profile · project history per Q2 ·
  `endpoint description`).
- **T2.3** Confirm layer + status-dependent option set; wire each option to its existing CLI
  fn (Attach/Start/View via rc; Fork via `cmd_fork`; Change-adapter/Instantiate via
  create-new re-entry; Resume + Kick per Q1/Q3 rulings).
- **T2.4** `s` keybind → write/update the `cc-<id>` shortcut (format per Q4) with baked flags
  (per Q5); add `--create` to `Run{}`.
- Unit: state-model transitions (category nav, filter narrowing, cursor windowing), the
  status→square mapping, the adapter→profile tree build, the shortcut-script content +
  create-vs-update, the flag→action mapping. Activate stages as evidence lands. Full suite +
  clippy -D + `traceable-reqs check` EXIT=0. W2 gate (doyle).

## Notes
- The picker is a pure front-end: it MUST NOT introduce a second bringup path — every
  terminal action routes through `cmd_endpoint_run` / the existing CLI fns. One bringup core,
  same invariant discipline as W1.5's one-pump.
- TUI testing: ratatui renders to a `Buffer` (testable without a real terminal) — assert the
  rendered buffer for screen-state unit tests; the live key loop is the manual-verify leg.
- ADR-0001 territory only if any wire/registry format changes (Q1 attach-presence op would
  be) — else clean-room. Additive `Run{}` flags are N-1-safe.
- UNCOMMITTED building until the operator calls a commit (their per-wave pacing).


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

# M12 Wave 2 — `spt endpoint run` interactive picker (ratatui) — JIT plan

> todlando 2026-06-14, post W1.5 + W3 gate (both PASS, committed 0c687fe / c02aabb). W2 is
> the BIGGEST remaining wave and the user-facing face of M12. **DESIGN-CHECK PENDING with
> doyle before building** — open questions below center on data surfaces the picker needs
> that may not exist yet (attached-state query, project history, per-endpoint session log,
> kick-and-attach op). Build only after his ruling. Full spec: `M12-ENDPOINT-RUN-PICKER.md`;
> wave scope: `M12-PLAN.md` Wave 2.

## Scope (M12-PLAN.md Wave 2 / picker spec)
- **T2.1** Layer 1 (kind: Create new | Pick existing) + create-new branch: choose harness
  adapter (profiles tree-nested under each adapter) → enter id (charset-validated) → start.
- **T2.2** Pick-existing: category L/R (`<project> | Local node | Subnet`), grouped +
  alpha-sorted, status squares (online green ■ / offline gray ▢ / attached blue ■),
  type-to-filter (`/`), pinned keybind legend, **two-pane right-half description**
  (adapter:profile · project history newest→oldest · `endpoint description`).
- **T2.3** Confirm layer + status-dependent options: Attach/Start/View · **Kick <node> and
  attach** (attached-only) · Instantiate-locally (remote) · Change-adapter (offline) · Fork ·
  **Resume-from-history** (offline-only; per-endpoint session log + manifest `resume`; titles
  `<project> @ <ts> (...id5)`).
- **T2.4** `s` keybind = build/update `cc-<id>` shortcut at project root with baked
  non-interactive flags. Requires the flag set to cover every terminal action of the flow.
- **Phone caveat:** don't hard-couple interaction to keybinds (future tap-mode layer can
  reuse the same state model + action enum).

## What EXISTS today (investigation 2026-06-14)
- **Terminal action core (the picker terminates INTO this):** `cli.rs:cmd_endpoint_run`
  (T1.1) — adapter resolve → `harnesshost::launch_harness_brokered_in` → start/attach/view
  via `rc::run_attach`. Picker = an interactive front-end that gathers (adapter, id,
  resume, action) then calls the same path. `Run{}` flags today: `--adapter <a[:profile]>`
  `--id` `--resume <session>` `--start|--attach|--view`. **No `--create` flag** (run mints
  fresh unless `--resume`); no kick/instantiate/fork-from-run flags.
- **Adapter + profile enumeration:** `registry::resolve_option` (split `<adapter>:<profile>`
  → parent lookup → overlay). Profiles live at `adapters/<adapter>/profiles/<profile>.toml`;
  `registry.rs:local_profiles_dir` + read_dir enumerate them (need a tidy "list harness
  adapters + their profiles" helper for L2; building blocks present). `AdapterKind::Harness`
  gates the harness-only list.
- **Endpoint discovery:** `roster::enumerate()` (local perches: state/ready/alive) +
  `roster::detect_self_id`; `wansend::load_snapshots(identity/registry)` per-subnet snapshots
  → `resource_projection(reg, hidden_fn)` = visible+routable rows `(endpoint_id, node, status,
  resources)`; `VisibilityStore` + `SubnetStore`. This is the L2 pick-existing data spine.
- **Status:** registry `Status{Active,Dormant,Offline,Suspended}` → online/offline mapping.
  **"Attached" (blue ■) has no obvious query** (see Q1).
- **Endpoint description:** `spt_store::info::read_info().resources` (the `endpoint
  description` blurb).
- **Deps:** `crossterm = "0.28"` already in `spt/Cargo.toml` (landed W1 for the rc raw-mode
  pump, explicitly noted "the same crate W2's ratatui picker builds on"). **ratatui +
  nucleo-matcher are NEW deps** to add this wave.
- **Reuse for terminal actions:** `cmd_fork` (Fork), `cmd_rest`/wake (resting), rc pump
  (attach/view) all exist as callable CLI fns.

## Investigation TODOs (read before coding — NOT yet done; gated on doyle scope ruling)
1. **`spt-daemon/src/attach.rs` + `presence.rs`**: is there a query "is endpoint E's
   broker PTY currently attached, and by which node/surface?" `serve_attach` serves a
   stream; PresenceLog tracks conn-level connect/disconnect. Determine whether attached-state
   (for the blue ■ + the "Kick <node>" option) is derivable from existing broker state or
   needs a new query op. (Drives Q1.)
2. **Project history surface**: where is a per-endpoint "projects worked, newest→oldest"
   list? Candidates: p-<project> context branches (contextstore), info.json, or the session
   log. The picker needs it for the description pane + confirm "Project history:". (Drives Q2.)
3. **Per-endpoint session log**: does a session-id log with last-message timestamps exist
   for Resume-from-history? `harnesshost::mint_session_id` mints; the adapter manifest
   `resume` declaration is the other half. Find where prior session ids are recorded (if at
   all) and what timestamp is available. (Drives Q3.)
4. **`vercel-labs/skills` `src/prompts/search-multiselect.ts`**: lift the state model
   `{query, cursor, selected:HashSet, locked:Vec}` + glyph set (`◆ ◇ ■ ● ○ ✓ • │ ─ ❯`) +
   windowed-cursor behavior. Map to ratatui `List`+`ListState`. (Look/feel parity.)
5. **`cmd_fork` / wake / change-adapter call shapes**: confirm the confirm-layer options can
   reuse the existing CLI fns (Fork, Instantiate-locally, Change-adapter, Resume) without
   new daemon plumbing beyond Q1/Q3 gaps.

## Open design questions for doyle
**Scope / data gaps (the load-bearing ones):**
1. **Attached-state (blue ■ + "Kick <node> and attach").** If no broker query exists for
   "who is attached to this PTY," this is the heaviest unknown. Options: **(a)** ship W2 with
   online/offline only, defer the blue tri-state + the Kick option to a follow-up once an
   attach-presence query exists; **(b)** build a broker attach-presence query op now (new
   daemon surface + the force-detach/kick wire op). Recommend **(a)** — keep W2 to the
   picker UX over surfaces that exist; the Kick path is a distinct broker capability with its
   own hazard surface (PTY ownership, KH zone) better scoped as its own slice. Confirm.
2. **Project history.** Is there an authoritative per-endpoint project list to read, or does
   W2 derive it from p-<project> context branches? If neither is cheap, propose rendering
   the description pane's history as "best-effort from contextstore, empty if none" and not
   blocking the wave on a new history store. Your call on the source.
3. **Resume-from-history.** Does a per-endpoint session-id log with timestamps exist? If not,
   Resume-from-history can't enumerate prior sessions honestly. Options: **(a)** defer
   Resume-from-history to when the session log exists (offline endpoints still get
   Attach/Start/View/Fork/Change-adapter); **(b)** establish a minimal per-endpoint session
   log now. Recommend **(a)** unless the log already exists.
4. **`cc-<id>` shortcut format (T2.4).** Cross-platform: a `.ps1` on Windows + a POSIX `sh`
   on Unix at project root, each baking the non-interactive `spt endpoint run` flags? Or a
   single portable form? Confirm the shape + the "Update if exists" overwrite semantics.
5. **Non-interactive flag completeness (T2.4 dependency).** The shortcut bakes the full
   selection. Today `Run{}` lacks `--create` (explicit create-vs-resume) and has no
   kick/instantiate/fork verbs. Proposal: add `--create` (explicit, the default-fresh made
   explicit) and let the shortcut cover only the *terminal* actions reachable
   non-interactively (adapter/profile + id + create|resume + start|attach|view); the
   interactive-only branches (Kick, Instantiate, Change-adapter, Fork) stay picker-only.
   Confirm the additive flag set.

**UX / structure (lower-stakes, my defaults unless you object):**
6. Single-binary in-process module `spt/src/picker/` (state model + ratatui render +
   key/action loop), invoked when `spt endpoint run` is called with no `--adapter`/`--id`
   (bare = interactive; flags present = today's non-interactive path, untouched). Keep the
   action enum the single source of truth so a future tap-mode layers on without touching it.
7. nucleo-matcher for `/` type-to-filter; ratatui two-column `Layout` for the description
   pane; keybind legend as a pinned bottom row. Esc=back, ←→=category, ↑↓=item, enter=confirm,
   /=filter, n=new, s=shortcut.

## Tentative tasks (pending doyle ruling — shapes WILL shift on Q1–Q5)
- **T2.0** Add REQs to `traceable-reqs.toml` FIRST (rule 3). Proposed: **REQ-RUN-PICKER**
  (interactive `spt endpoint run` TUI: kind layer, create-new adapter→profile→id, pick-existing
  category/group/status/filter/two-pane, confirm layer + status-dependent options) and
  **REQ-RUN-SHORTCUT** (`cc-<id>` generation + the additive non-interactive flag set).
  Granularity per doyle. Stages doc+impl+unit this wave (TUI int is harness-driven /
  manual — confirm whether `int` applies or stays a manual-verify like REQ-PAIR-6's OS probe).
- **T2.1** Add ratatui + nucleo-matcher deps. `spt/src/picker/` module: state model
  (`{query,cursor,selected,locked}` lifted) + the Layer-1 kind screen + create-new branch
  (harness-adapter list with profiles tree-nested → id-entry with live charset validation) →
  call `cmd_endpoint_run`. Glyph set + windowed cursor for feel parity.
- **T2.2** Pick-existing screen: category L/R over `[<cwd-project> | Local node | Subnet]`,
  grouping + alpha-sort per spec, status squares (scope per Q1), `/` filter (nucleo),
  pinned legend, two-pane description (adapter:profile · project history per Q2 ·
  `endpoint description`).
- **T2.3** Confirm layer + status-dependent option set; wire each option to its existing CLI
  fn (Attach/Start/View via rc; Fork via `cmd_fork`; Change-adapter/Instantiate via
  create-new re-entry; Resume + Kick per Q1/Q3 rulings).
- **T2.4** `s` keybind → write/update the `cc-<id>` shortcut (format per Q4) with baked flags
  (per Q5); add `--create` to `Run{}`.
- Unit: state-model transitions (category nav, filter narrowing, cursor windowing), the
  status→square mapping, the adapter→profile tree build, the shortcut-script content +
  create-vs-update, the flag→action mapping. Activate stages as evidence lands. Full suite +
  clippy -D + `traceable-reqs check` EXIT=0. W2 gate (doyle).

## Notes
- The picker is a pure front-end: it MUST NOT introduce a second bringup path — every
  terminal action routes through `cmd_endpoint_run` / the existing CLI fns. One bringup core,
  same invariant discipline as W1.5's one-pump.
- TUI testing: ratatui renders to a `Buffer` (testable without a real terminal) — assert the
  rendered buffer for screen-state unit tests; the live key loop is the manual-verify leg.
- ADR-0001 territory only if any wire/registry format changes (Q1 attach-presence op would
  be) — else clean-room. Additive `Run{}` flags are N-1-safe.
- UNCOMMITTED building until the operator calls a commit (their per-wave pacing).


Plan written. Ping doyle for design-check.


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && $OWL send doyle <<'EOF'
M12 W2 JIT plan up for design-check: M12-W2-PLAN.md (the `spt endpoint run` interactive ratatui picker — biggest remaining M12 wave).

Built on the W1.5/W3 pattern: scope, what-exists, investigation TODOs, open questions. 5 load-bearing design Qs gate the build — all about data surfaces the picker spec wants that may NOT exist yet:

  Q1 attached-state (blue ■ + "Kick <node> and attach") — no broker "who's attached" query found. Recommend (a) ship online/offline only, defer blue tri-state + Kick to its own slice (distinct broker capability, PTY-ownership hazard zone).
  Q2 project history (description pane) — source? derive best-effort from contextstore, don't block wave on a new history store.
  Q3 resume-from-history — needs a per-endpoint session-id log w/ timestamps; if absent, recommend defer (offline still gets Attach/Start/View/Fork/Change-adapter).
  Q4 cc-<id> shortcut format — .ps1 (Win) + sh (Unix) baking non-interactive flags? overwrite-if-exists semantics?
  Q5 non-interactive flag completeness — add `--create` to Run{}; shortcut covers only terminal actions reachable non-interactively, interactive-only branches stay picker-only.

Plus: proposed REQ-RUN-PICKER + REQ-RUN-SHORTCUT (granularity your call); question whether TUI gets `int` stage or manual-verify like REQ-PAIR-6.

Net: the cuts (Q1/Q3 defer) keep W2 a pure front-end over surfaces that exist — one bringup core, no second path. Want your scope ruling before I build. Plan's got the full detail.
EOF
echo "EXIT=$?"


➜ SENT:doyle
EXIT=0


Plan written + design-check sent to doyle. Status:

**M12-W2-PLAN.md** — `spt endpoint run` interactive ratatui picker. Biggest remaining M12 wave.

Followed W1.5/W3 template: scope · what-exists · investigation TODOs · open Qs · tentative tasks · gate.

**Key finding:** picker spec wants data surfaces that may not exist yet. 5 load-bearing Qs sent to doyle:
- **Q1** attached-state (blue ■ + "Kick node") — no broker "who's attached" query found → recommend defer, ship online/offline only
- **Q2** project history source → best-effort from contextstore, don't block wave
- **Q3** resume-from-history → needs per-endpoint session log; defer if absent
- **Q4** `cc-<id>` shortcut format (.ps1 + sh, overwrite semantics)
- **Q5** add `--create` to `Run{}`; shortcut covers terminal actions only

The Q1/Q3 cuts keep W2 a **pure front-end** over existing surfaces — one bringup core, no second path (same discipline as W1.5 one-pump).

Also flagged: proposed REQ-RUN-PICKER + REQ-RUN-SHORTCUT, and whether TUI gets `int` stage vs manual-verify (REQ-PAIR-6 precedent).

Waiting on doyle's scope ruling. Build on greenlight. Perch live — his reply lands here.


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="ad00168c" type="msg" from="doyle">W2 DESIGN RULING (doyle) — verified surfaces vs source first. Full: M12-W2-RULING.md. HEADLINE: one correction — Resume-from-history is NOT deferred.

Q1 attached-state (blue ■ + Kick): DEFER (a) ✓ agree. W2 = online/offline only. Blue tri-state + Kick need a new broker attach-presence query + force-detach</EVENT-PART>
<EVENT-PART seq="2/8" id="ad00168c"> wire op = distinct broker mutation in PTY-ownership hazard zone → its own slice+gate, not a picker-wave rider.

Q2 project history pane: best-effort from existing contextstore (p-&lt;project&gt; branches), empty-if-none. No new history store. Informational pane, degrade-to-empty.

Q3 Resume-from-history: BUILD IT — do NOT defe</EVENT-PART>
<EVENT-PART seq="3/8" id="ad00168c">r. CORRECTION: your TODO#3 wasn't run — the session ledger EXISTS: spt-store/src/sessions.rs last_k(perch,k) → Vec&lt;SessionEntry{ts RFC3339, session_id, trigger}&gt;, bounded, from M10/ADR-0019/REQ-TERM-6. AND --resume already on Run{}. So picker enumerates last_k for the offline LOCAL endpoint, renders &lt;project&gt; @ &lt;ts&gt; (…i</EVENT-PART>
<EVENT-PART seq="4/8" id="ad00168c">d5), feeds session_id → cmd_endpoint_run --resume. NO new daemon surface — pure front-end over existing surfaces = meets your own keep-bar. Offline+LOCAL-only (ledger at local perch; remote-offline = Instantiate not Resume). Adapter resume-capability is its manifest's job, surfaced at runtime not a picker gap. Empty ledger ⇒ no Resume op</EVENT-PART>
<EVENT-PART seq="5/8" id="ad00168c">tion.

Q4 cc-&lt;id&gt;: per-OS native (.ps1 Win / sh+chmod Unix — single portable form can't be both), current-OS only. Overwrite = SENTINEL-guarded: generator writes+checks a generated-by header marker; overwrite our-own freely, REFUSE+warn if a same-named file lacks the sentinel (don't clobber user file).

Q5 flags: approve ad</EVENT-PART>
<EVENT-PART seq="6/8" id="ad00168c">ditive --create (N-1-safe). Shortcut = terminal actions only: adapter[:profile]+id+(create|resume)+(start|attach|view). Resume IS bakeable (ledger+--resume exist). Kick/Instantiate/Change-adapter/Fork stay picker-only.

REQ: REQ-RUN-PICKER + REQ-RUN-SHORTCUT, 2-REQ granularity approved. Stages doc+impl+unit, NO int on the TUI — ratatui</EVENT-PART>
<EVENT-PART seq="7/8" id="ad00168c"> renders to a testable Buffer, assert rendered buffer + state-model transitions as unit; live key loop = manual-verify leg (note like REQ-PAIR-6). Bringup path already int-covered (W1/W1.5), picker rides it.

Q6/Q7 UX defaults: APPROVED (in-process spt/src/picker/, bare=interactive flags-present=untouched, action-enum single source of tr</EVENT-PART>
<EVENT-PART seq="8/8" id="ad00168c">uth, nucleo filter, ratatui layout).

BINDING GATE INVARIANT: NO second bringup path — every terminal action routes through cmd_endpoint_run / existing CLI fns. One bringup core, same discipline as one-pump. I gate on thi
...(truncated)</event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


doyle GREENLIGHT + corrections. Read full ruling (truncated).


1	# M12 W2 — doyle design ruling (picker)
2
3	> doyle 2026-06-14, gating todlando's W2 JIT plan (M12-W2-PLAN.md). Claims verified vs source
4	> before ruling. **Headline correction: Resume-from-history is NOT deferred — its data surface
5	> already exists.**
6
7	## Verified against source
8	- **Session ledger EXISTS** — `spt-store/src/sessions.rs`: `last_k(perch, k) -> Vec<SessionEntry{ ts: RFC3339-UTC, session_id, trigger(Boot|Clear|Compact) }>`, oldest→newest, bounded (MAX_LEDGER=64), garbage-tolerant. Landed M10 (ADR-0019 / REQ-TERM-6) for digest thread-spanning. This IS the per-endpoint session-id log WITH timestamps Q3 assumed might not exist. (Investigation TODO#3 wasn't run — it does exist.)
9	- **`--resume <session>` flag EXISTS** on `Run{}` (plan line 32) → the terminal path for resume is already there.
10	- **No attach-presence query** — `presence.rs` has snapshots / most_recently_active / resolve, no "which node is attached to endpoint E's PTY." Q1's gap is real.
11
12	## Rulings
13
14	**Q1 — attached-state (blue ■ + "Kick <node> and attach"): DEFER. Option (a).**
15	W2 ships **online/offline only**. The blue tri-state + Kick need a new broker attach-presence query AND a force-detach/kick wire op — a distinct broker mutation surface in the PTY-ownership hazard zone (KNOWN-HAZARDS). That earns its own focused slice + gate, not a rider on the picker UX wave. Confirm deferred to a follow-up.
16
17	**Q2 — project-history description pane: best-effort, no new store.**
18	Derive newest→oldest from the existing contextstore (`p-<project>` branches); render empty if none. Do NOT block W2 on a new history store. It's an informational pane — degrade-to-empty is fine.
19
20	**Q3 — Resume-from-history: BUILD IT (correction — do NOT defer).**
21	The session ledger exists and `--resume` exists. The picker enumerates `sessions::last_k` for the offline LOCAL endpoint, renders titles `<project> @ <ts> (…id5)`, and feeds the chosen `session_id` to `cmd_endpoint_run --resume`. **No new daemon surface.** This is a pure front-end over surfaces that exist — it meets your own "stays in W2" bar. Notes:
22	- Offline-only + LOCAL-only (the ledger is at the local perch; you resume on a node you own). A remote offline endpoint = Instantiate-locally, not Resume.
23	- Whether a given adapter actually resumes is its manifest `resume` declaration's job — surfaced at run time, not a picker-scope gap. The picker offers Resume when ledger rows exist; an adapter that can't resume fails at the terminal action, same as any capability mismatch.
24	- Empty ledger ⇒ no Resume option (offline still gets Attach/Start/View/Fork/Change-adapter).
25
26	**Q4 — `cc-<id>` shortcut format: per-OS native + sentinel-guarded overwrite.**
27	Generate the **current-OS** script: `.ps1` on Windows, POSIX `sh` (+`chmod +x`) on Unix — a single portable form can't be both. Bake the non-interactive flags. **Overwrite semantics:** the shortcut is a derived artifact — regenerate freely, BUT only overwrite a file carrying a generated-by **sentinel** (a header comment marker the generator writes + checks). If a same-named file lacks the sentinel, refuse + warn (don't clobber a user file). "Update if exists" = overwrite-our-own-only.
28
29	**Q5 — non-interactive flags: approve additive `--create`.**
30	Add `--create` to `Run{}` (explicit fresh, the default-fresh made explicit; N-1-safe additive). Shortcut covers only **terminal actions reachable non-interactively**: adapter[:profile] + id + (create|resume) + (start|attach|view). Resume IS in this set (ledger + `--resume` exist). Interactive-only branches (Kick, Instantiate-locally, Change-adapter, Fork) stay picker-only — not bakeable. Confirm.
31
32	## Structure / REQ
33	- **REQs:** REQ-RUN-PICKER (interactive TUI) + REQ-RUN-SHORTCUT (`cc-<id>` gen + additive flags) — two REQs is the right granularity (separable surfaces). Approve.
34	- **Stages: doc + impl + unit. NO `int` on the TUI.** ratatui renders to a testable `Buffer` — assert the rendered buffer for screen-state + state-model transitions (category nav, filter narrowing, cursor windowing, status→square, adapter→profile tree, ledger→resume-titles, shortcut content + create-vs-update, flag→action). The live key loop is a **manual-verify leg** (like REQ-PAIR-6's OS probe — note it in the plan). The integrating bringup path is ALREADY int-covered (W1/W1.5 `cmd_endpoint_run`); the picker rides it.
35	- **Q6/Q7 UX defaults: approved.** In-process `spt/src/picker/`; bare `spt endpoint run` (no `--adapter`/`--id`) = interactive, flags-present = today's non-interactive path untouched; action enum = single source of truth (tap-mode layers on later); nucleo `/` filter; ratatui two-column layout; pinned legend.
36
37	## Binding invariant (gate condition)
38	**No second bringup path.** Every terminal action routes through `cmd_endpoint_run` / existing CLI fns (`cmd_fork`, wake, rc pump). One bringup core — same discipline as W1.5's one-pump. I gate on this.
39
40	## Gate posture
41	Build uncommitted on m12-w1-bringup-rc. I gate (reproduce traceable + read the load-bearing state-model/buffer tests + confirm single-bringup-path) before any commit. Suite + clippy -D + traceable EXIT=0. Operator calls the commit.
42

# M12 W2 — doyle design ruling (picker)

> doyle 2026-06-14, gating todlando's W2 JIT plan (M12-W2-PLAN.md). Claims verified vs source
> before ruling. **Headline correction: Resume-from-history is NOT deferred — its data surface
> already exists.**

## Verified against source
- **Session ledger EXISTS** — `spt-store/src/sessions.rs`: `last_k(perch, k) -> Vec<SessionEntry{ ts: RFC3339-UTC, session_id, trigger(Boot|Clear|Compact) }>`, oldest→newest, bounded (MAX_LEDGER=64), garbage-tolerant. Landed M10 (ADR-0019 / REQ-TERM-6) for digest thread-spanning. This IS the per-endpoint session-id log WITH timestamps Q3 assumed might not exist. (Investigation TODO#3 wasn't run — it does exist.)
- **`--resume <session>` flag EXISTS** on `Run{}` (plan line 32) → the terminal path for resume is already there.
- **No attach-presence query** — `presence.rs` has snapshots / most_recently_active / resolve, no "which node is attached to endpoint E's PTY." Q1's gap is real.

## Rulings

**Q1 — attached-state (blue ■ + "Kick <node> and attach"): DEFER. Option (a).**
W2 ships **online/offline only**. The blue tri-state + Kick need a new broker attach-presence query AND a force-detach/kick wire op — a distinct broker mutation surface in the PTY-ownership hazard zone (KNOWN-HAZARDS). That earns its own focused slice + gate, not a rider on the picker UX wave. Confirm deferred to a follow-up.

**Q2 — project-history description pane: best-effort, no new store.**
Derive newest→oldest from the existing contextstore (`p-<project>` branches); render empty if none. Do NOT block W2 on a new history store. It's an informational pane — degrade-to-empty is fine.

**Q3 — Resume-from-history: BUILD IT (correction — do NOT defer).**
The session ledger exists and `--resume` exists. The picker enumerates `sessions::last_k` for the offline LOCAL endpoint, renders titles `<project> @ <ts> (…id5)`, and feeds the chosen `session_id` to `cmd_endpoint_run --resume`. **No new daemon surface.** This is a pure front-end over surfaces that exist — it meets your own "stays in W2" bar. Notes:
- Offline-only + LOCAL-only (the ledger is at the local perch; you resume on a node you own). A remote offline endpoint = Instantiate-locally, not Resume.
- Whether a given adapter actually resumes is its manifest `resume` declaration's job — surfaced at run time, not a picker-scope gap. The picker offers Resume when ledger rows exist; an adapter that can't resume fails at the terminal action, same as any capability mismatch.
- Empty ledger ⇒ no Resume option (offline still gets Attach/Start/View/Fork/Change-adapter).

**Q4 — `cc-<id>` shortcut format: per-OS native + sentinel-guarded overwrite.**
Generate the **current-OS** script: `.ps1` on Windows, POSIX `sh` (+`chmod +x`) on Unix — a single portable form can't be both. Bake the non-interactive flags. **Overwrite semantics:** the shortcut is a derived artifact — regenerate freely, BUT only overwrite a file carrying a generated-by **sentinel** (a header comment marker the generator writes + checks). If a same-named file lacks the sentinel, refuse + warn (don't clobber a user file). "Update if exists" = overwrite-our-own-only.

**Q5 — non-interactive flags: approve additive `--create`.**
Add `--create` to `Run{}` (explicit fresh, the default-fresh made explicit; N-1-safe additive). Shortcut covers only **terminal actions reachable non-interactively**: adapter[:profile] + id + (create|resume) + (start|attach|view). Resume IS in this set (ledger + `--resume` exist). Interactive-only branches (Kick, Instantiate-locally, Change-adapter, Fork) stay picker-only — not bakeable. Confirm.

## Structure / REQ
- **REQs:** REQ-RUN-PICKER (interactive TUI) + REQ-RUN-SHORTCUT (`cc-<id>` gen + additive flags) — two REQs is the right granularity (separable surfaces). Approve.
- **Stages: doc + impl + unit. NO `int` on the TUI.** ratatui renders to a testable `Buffer` — assert the rendered buffer for screen-state + state-model transitions (category nav, filter narrowing, cursor windowing, status→square, adapter→profile tree, ledger→resume-titles, shortcut content + create-vs-update, flag→action). The live key loop is a **manual-verify leg** (like REQ-PAIR-6's OS probe — note it in the plan). The integrating bringup path is ALREADY int-covered (W1/W1.5 `cmd_endpoint_run`); the picker rides it.
- **Q6/Q7 UX defaults: approved.** In-process `spt/src/picker/`; bare `spt endpoint run` (no `--adapter`/`--id`) = interactive, flags-present = today's non-interactive path untouched; action enum = single source of truth (tap-mode layers on later); nucleo `/` filter; ratatui two-column layout; pinned legend.

## Binding invariant (gate condition)
**No second bringup path.** Every terminal action routes through `cmd_endpoint_run` / existing CLI fns (`cmd_fork`, wake, rc pump). One bringup core — same discipline as W1.5's one-pump. I gate on this.

## Gate posture
Build uncommitted on m12-w1-bringup-rc. I gate (reproduce traceable + read the load-bearing state-model/buffer tests + confirm single-bringup-path) before any commit. Suite + clippy -D + traceable EXIT=0. Operator calls the commit.


Full ruling clear — GREENLIGHT. Corrections: **Resume-from-history = BUILD** (session ledger exists), Q1 Kick/blue deferred, rest approved. ACK doyle + read load-bearing source surfaces.


cd "C:\Users\decid\Documents\projects\spt-core" && $OWL send doyle <<'EOF'
W2 ruling absorbed — thanks for verifying source first. Corrections taken:
- Q3 Resume-from-history: BUILDING it. Caught my un-run TODO#3 — sessions::last_k ledger exists (M10/REQ-TERM-6), --resume on Run{} exists. Offline+LOCAL-only, empty-ledger⇒no-option, adapter resume-capability is manifest's job. Pure front-end, no new daemon surface.
- Q1 Kick/blue: deferred to its own broker attach-presence+force-detach slice (confirmed).
- Q2 history pane best-effort contextstore degrade-to-empty; Q4 per-OS native + sentinel-guarded overwrite; Q5 additive --create, terminal-actions-only shortcut (resume bakeable).
- 2 REQs, doc+impl+unit NO int (Buffer + state-model unit asserts; live loop = manual-verify note per REQ-PAIR-6); single-bringup-path invariant = the gate I build to.

Updating plan w/ crystallized post-ruling design, adding REQs to toml, then building uncommitted. Will ping when ready to gate.
EOF
echo "EXIT=$?"