1258	pub(crate) fn cmd_endpoint_run(
1259	    adapter: &str,
1260	    id: &str,
1261	    resume: Option<String>,
1262	    start: bool,
1263	    // `--attach` is the default terminal action; the bool is accepted for flag
1264	    // symmetry but `start`/`view` are what branch (attach = neither).
1265	    _attach: bool,
1266	    view: bool,
1267	) -> i32 {
1268	    // id charset (the harness binds to exactly this id via argv).
1269	    // [impl->REQ-HAZARD-ID-CHARSET]
1270	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
1271	        eprintln!("INVALID_ID:{id}: {e}");
1272	        return 2;
1273	    }
1274	    // Resolve the harness adapter option (`<adapter>[:profile]`) through the
1275	    // merged view (split → parent lookup → overlay), exactly like shell spawn.
1276	    let adapters_dir = spt_store::perch::adapters_dir();
1277	    let manifest = match spt_runtime::registry::resolve_option(&adapters_dir, adapter) {
1278	        Ok((_, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => m,
1279	        Ok(_) => {
1280	            eprintln!(
1281	                "ENDPOINT_RUN_NOT_HARNESS:{adapter}: not a kind=\"harness\" adapter — \
1282	                 `endpoint run` brings up harness endpoints (shells use `shell spawn`)"
1283	            );
1284	            return 1;
1285	        }
1286	        Err(e) => {
1287	            eprintln!(
1288	                "ENDPOINT_RUN_ADAPTER_UNREGISTERED:{adapter}: not an active registered \
1289	                 harness adapter / valid profile on this node ({e}; spt adapter list)"
1290	            );
1291	            return 1;
1292	        }
1293	    };
1294
1295	    // A fresh bringup mints a provisional spawn-time session id; --resume carries
1296	    // a prior one (the harness reports the real id post-spawn either way).
1297	    let session_id = resume.unwrap_or_else(spt_daemon::harnesshost::mint_session_id);
1298
1299	    if let Err(e) = spt_daemon::daemon::ensure_running() {
1300	        eprintln!("ENDPOINT_RUN_DAEMON_FAIL:{id}: {e}");
1301	        return 1;
1302	    }
1303	    // REQ-HOST-RUN-2: land the harness PTY in the PROJECT cwd — where the user
1304	    // invoked `spt endpoint run` (`cc <id>` at a project root) — NOT the
1305	    // daemon's cwd. A project-scoped harness (Claude Code) reads its `.claude`,
1306	    // session history, and digest source relative to cwd. A failure to read the
1307	    // cwd falls back to the broker-inherited cwd (None) rather than aborting.
1308	    let project_cwd = std::env::current_dir()
1309	        .ok()
1310	        .map(|p| p.to_string_lossy().into_owned());
1311	    match spt_daemon::harnesshost::launch_harness_brokered_in(
1312	        &spt_daemon::endpoint::broker_socket_name(),
1313	        id,
1314	        adapter,
1315	        &session_id,
1316	        &manifest,
1317	        project_cwd.as_deref(),
1318	    ) {
1319	        Ok(spawned) => {
1320	            eprintln!(
1321	                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \
1322	                 (harness binds its perch on startup)",
1323	                spawned.pid
1324	            );
1325	        }
1326	        Err(e) => {
1327	            eprintln!("ENDPOINT_RUN_FAIL:{id}: {e}");
1328	            return 1;
1329	        }
1330	    }
1331
1332	    // Terminal action: start (return now) | view (read-only attach) | attach
1333	    // (default). Attach/view route through the SAME rc pump as `spt rc`.
1334	    if start {
1335	        // `ENDPOINT_RUN_STARTED:{id}` is the machine token (no Markdown markers →
1336	        // survives strip intact for a piped adapter); the `spt rc` hint restyles
1337	        // on a tty and strips clean otherwise. [impl->REQ-CLI-OUTPUT-MARKDOWN]
1338	        eprintln!(
1339	            "{}",
1340	            crate::helpfmt::render(
1341	                &format!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)"),
1342	                crate::helpfmt::stderr_color()
1343	            )
1344	        );
1345	        return 0;
1346	    }
1347	    match crate::rc::run_attach(id, rc_intent(view, false)) {

pub(crate) 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 {
        // `ENDPOINT_RUN_STARTED:{id}` is the machine token (no Markdown markers →
        // survives strip intact for a piped adapter); the `spt rc` hint restyles
        // on a tty and strips clean otherwise. [impl->REQ-CLI-OUTPUT-MARKDOWN]
        eprintln!(
            "{}",
            crate::helpfmt::render(
                &format!("ENDPOINT_RUN_STARTED:{id} (attach with `spt rc {id}`)"),
                crate::helpfmt::stderr_color()
            )
        );
        return 0;
    }
    match crate::rc::run_attach(id, rc_intent(view, false)) {


320	fn render_confirm(model: &PickerModel, f: &mut Frame, area: Rect) {
321	    let chunks = Layout::default()
322	        .direction(Direction::Vertical)
323	        .constraints([Constraint::Length(7), Constraint::Min(1), Constraint::Length(1)])
324	        .split(area);
325
326	    // The selection summary block.
327	    if let Some(ep) = model.selected_endpoint() {
328	        let status_word = match ep.status {
329	            EpStatus::Online => "online",
330	            EpStatus::Offline => "offline",
331	        };
332	        let loc = if ep.is_local {
333	            "LOCAL".to_string()
334	        } else {
335	            ep.node.clone()
336	        };
337	        let history = if ep.project_history.is_empty() {
338	            "(none)".to_string()
339	        } else {
340	            ep.project_history.join(", ")
341	        };
342	        // The W2.5 controlled-by pin (REQ-RCVIEW-1): `controlled by <node>
343	        // (+N viewing)` when a controller holds the endpoint.
344	        let control_line = match &ep.driven_by {
345	            Some(node) if ep.viewer_count > 0 => {
346	                format!("controlled by {node} (+{} viewing)", ep.viewer_count)
347	            }
348	            Some(node) => format!("controlled by {node}"),
349	            None if ep.viewer_count > 0 => format!("(+{} viewing)", ep.viewer_count),
350	            None => String::new(),
351	        };
352	        let summary = vec![
353	            Line::from(Span::styled(
354	                ep.id.clone(),
355	                Style::default().add_modifier(Modifier::BOLD),
356	            )),
357	            Line::from(vec![
358	                square_span(ep),
359	                Span::raw(format!(" {status_word} • {loc}")),
360	            ]),
361	            Line::from(control_line),
362	            Line::from(ep.adapter_profile.clone()),
363	            Line::from(format!("Project history: {history}")),
364	            Line::from(ep.description.clone()),

fn render_confirm(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(7), Constraint::Min(1), Constraint::Length(1)])
        .split(area);

    // The selection summary block.
    if let Some(ep) = model.selected_endpoint() {
        let status_word = match ep.status {
            EpStatus::Online => "online",
            EpStatus::Offline => "offline",
        };
        let loc = if ep.is_local {
            "LOCAL".to_string()
        } else {
            ep.node.clone()
        };
        let history = if ep.project_history.is_empty() {
            "(none)".to_string()
        } else {
            ep.project_history.join(", ")
        };
        // The W2.5 controlled-by pin (REQ-RCVIEW-1): `controlled by <node>
        // (+N viewing)` when a controller holds the endpoint.
        let control_line = match &ep.driven_by {
            Some(node) if ep.viewer_count > 0 => {
                format!("controlled by {node} (+{} viewing)", ep.viewer_count)
            }
            Some(node) => format!("controlled by {node}"),
            None if ep.viewer_count > 0 => format!("(+{} viewing)", ep.viewer_count),
            None => String::new(),
        };
        let summary = vec![
            Line::from(Span::styled(
                ep.id.clone(),
                Style::default().add_modifier(Modifier::BOLD),
            )),
            Line::from(vec![
                square_span(ep),
                Span::raw(format!(" {status_word} • {loc}")),
            ]),
            Line::from(control_line),
            Line::from(ep.adapter_profile.clone()),
            Line::from(format!("Project history: {history}")),
            Line::from(ep.description.clone()),


Read data.rs (how driven_by/node gathered) + node_label_display def + check ENDPOINT_RUN: consumers.


1	//! Picker **data gather** — read-only projections over the EXISTING surfaces
2	//! the rest of the CLI already uses. No new daemon surface, no new store
3	//! (doyle ruling): adapters from the registry, endpoints from the roster + the
4	//! WAN registry snapshots, descriptions from `info.json`, project history
5	//! best-effort from the context store, resume rows from the session ledger.
6	//!
7	//! Everything here degrades to empty rather than failing — the picker is a
8	//! front-end; a missing surface means "nothing to show", never a crash.
9	// [impl->REQ-RUN-PICKER]
10
11	use std::path::Path;
12
13	use spt_store::perch::{self, ParentHint};
14	use spt_store::{info, project, sessions};
15
16	use super::model::{AdapterInfo, AdapterOption, EndpointRow, EpStatus, ResumeRow};
17
18	/// The cwd project name — the Project category label + the project-history match
19	/// key. Derived exactly like the daemon derives it (remote-url slug → toplevel
20	/// folder → dir name).
21	pub fn project_name() -> String {
22	    let dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
23	    project::project_id_for_dir(&dir)
24	}
25
26	/// The selectable adapter tree: every active `kind="harness"` adapter with its
27	/// shipped + local profiles nested under it.
28	pub fn adapter_options() -> Vec<AdapterOption> {
29	    let adapters_dir = perch::adapters_dir();
30	    let mut infos: Vec<AdapterInfo> = Vec::new();
31	    for (record, manifest) in spt_runtime::registry::registered(&adapters_dir) {
32	        if manifest.adapter.kind != spt_runtime::manifest::AdapterKind::Harness {
33	            continue;
34	        }
35	        let mut shipped: Vec<String> = manifest.profiles.keys().cloned().collect();
36	        shipped.sort();
37	        let local = spt_runtime::registry::local_profile_names(&adapters_dir, &record.name);
38	        infos.push(AdapterInfo {
39	            name: record.name,
40	            shipped,
41	            local,
42	        });
43	    }
44	    infos.sort_by(|a, b| a.name.cmp(&b.name));
45	    super::model::build_adapter_tree(&infos)
46	}
47
48	/// The shortcut basename an adapter option declares in its RESOLVED manifest
49	/// (`[adapter] shortcut_basename`, REQ-MANIFEST-7), defaulting to the
50	/// harness-agnostic [`DEFAULT_BASENAME`](super::shortcut::DEFAULT_BASENAME)
51	/// (`spt`) when absent or the adapter is unresolvable. This is the seam that
52	/// lets spt-claude-code brand its shortcuts `cc-<id>` without spt-core ever
53	/// hardcoding a harness name.
54	// [impl->REQ-MANIFEST-7]
55	pub fn shortcut_basename(adapter_option: &str) -> String {
56	    let adapters_dir = perch::adapters_dir();
57	    spt_runtime::registry::resolve_option(&adapters_dir, adapter_option)
58	        .ok()
59	        .and_then(|(_, m)| m.adapter.shortcut_basename)
60	        .filter(|b| !b.trim().is_empty())
61	        .unwrap_or_else(|| super::shortcut::DEFAULT_BASENAME.to_string())
62	}
63
64	/// Every discoverable endpoint, across all three categories. The model
65	/// partitions by category; this just produces the union (local perches +
66	/// subnet registry rows).
67	// [impl->REQ-RUN-PICKER]
68	pub fn gather_endpoints() -> Vec<EndpointRow> {
69	    let local = local_rows();
70	    let subnet = subnet_rows();
71	    let mut rows = Vec::with_capacity(local.len() + subnet.len());
72	    rows.extend(local);
73	    rows.extend(subnet);
74	    reconcile_self_owned(&mut rows);
75	    rows
76	}
77
78	/// Reconcile the status of a self-owned endpoint that is dual-listed in both the
79	/// Local and Subnet categories (REQ-PICKER-3). The two listings draw from
80	/// independent sources — Local from the live roster (ground truth for an endpoint
81	/// THIS node hosts), Subnet from the persisted WAN snapshot (a lagged projection)
82	/// — so a stale snapshot can disagree with the roster and paint the SAME endpoint
83	/// online in one category and offline in the other (defect 3). The roster wins:
84	/// for any subnet row whose id matches a local (is_local) row, override the
85	/// status/controllable/endpoint_type with the local fact. Both listings are kept
86	/// (distinct views); only the status square is unified.
87	// [impl->REQ-PICKER-3]
88	fn reconcile_self_owned(rows: &mut [EndpointRow]) {
89	    use std::collections::HashMap;
90	    let local: HashMap<String, (EpStatus, Option<bool>, String)> = rows
91	        .iter()
92	        .filter(|r| r.is_local)
93	        .map(|r| (r.id.clone(), (r.status, r.controllable, r.endpoint_type.clone())))
94	        .collect();
95	    for row in rows.iter_mut() {
96	        if row.is_local {
97	            continue;
98	        }
99	        if let Some((status, controllable, endpoint_type)) = local.get(&row.id) {
100	            row.status = *status;
101	            row.controllable = *controllable;
102	            row.endpoint_type = endpoint_type.clone();
103	        }
104	    }
105	}
106
107	/// This node's local perches (from the roster). Local rows feed both the Local
108	/// and Project categories (the model filters Project by project-history match).
109	fn local_rows() -> Vec<EndpointRow> {
110	    crate::roster::enumerate()
111	        .into_iter()
112	        .map(|p| {
113	            let perch_path = perch::resolve_perch_path(&p.id, ParentHint::Infer);
114	            let rec = info::read_info(&perch_path);
115	            let status = if p.alive {
116	                EpStatus::Online
117	            } else {
118	                EpStatus::Offline
119	            };
120	            let adapter_profile = rec
121	                .as_ref()
122	                .and_then(|r| r.adapter.clone())
123	                .unwrap_or_else(|| "(unknown adapter)".to_string());
124	            let description = rec
125	                .as_ref()
126	                .and_then(|r| r.resources.clone())
127	                .unwrap_or_default();
128	            // Project membership = committed-context projects (history) UNION the
129	            // endpoint's ORIGIN project (the cwd it was created in). A FRESH local
130	            // endpoint has no context branch yet, but it still belongs to the
131	            // project it was created in — so it appears in that project's picker
132	            // category immediately (REQ-PICKER-HISTORY-FRESH). Derive the origin
133	            // exactly like the daemon / project_name() (project_id_for_dir) so the
134	            // ids match the Project-category filter key.
135	            let origin_project = rec
136	                .as_ref()
137	                .and_then(|r| r.cwd.as_deref())
138	                .map(|cwd| project::project_id_for_dir(Path::new(cwd)));
139	            let project_history =
140	                merge_origin_project(project_history_for(&p.id), origin_project);
141	            // Resume-from-history is offline + LOCAL only (doyle Q3): only an
142	            // offline local endpoint reads its ledger.
143	            let resume_rows = if status == EpStatus::Offline {
144	                resume_rows_for(&perch_path, &project_history)
145	            } else {
146	                Vec::new()
147	            };
148	            EndpointRow {
149	                id: p.id,
150	                group: "local".to_string(),
151	                node: "LOCAL".to_string(),
152	                status,
153	                is_local: true,
154	                adapter_profile,
155	                project_history,
156	                description,
157	                resume_rows,
158	                driven_by: rec.as_ref().and_then(|r| r.driven_by.clone()),
159	                viewer_count: rec.as_ref().and_then(|r| r.viewer_count).unwrap_or(0),
160	                // Broker-PTY-controllability + type for the 4-state display
161	                // (REQ-PICKER-1) — read off this local perch's info.json.
162	                controllable: rec.as_ref().and_then(|r| r.controllable),
163	                endpoint_type: rec.as_ref().map(|r| r.state.clone()).unwrap_or_default(),
164	            }
165	        })
166	        .collect()
167	}
168
169	/// Subnet endpoints, from the WAN registry snapshots, visibility-gated exactly
170	/// like `spt endpoint list`.
171	fn subnet_rows() -> Vec<EndpointRow> {
172	    use spt_net::net::registry::resource_projection;
173	    use spt_store::visibility::VisibilityStore;
174
175	    let subnets = spt_store::subnet::SubnetStore::load();
176	    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
177	    let vis = VisibilityStore::load();
178	    let mut out = Vec::new();
179	    for s in &subnets.subnets {
180	        let Some(reg) = regs.get(&s.name) else {
181	            continue;
182	        };
183	        let hide_new = s.hide_new_endpoints;
184	        for row in resource_projection(reg, |id| vis.hidden(id, &s.name, hide_new)) {
185	            let status = match row.status {
186	                spt_net::net::registry::Status::Active
187	                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
188	                // Suspended (cold) + Offline render offline in the W2 duality.
189	                _ => EpStatus::Offline,
190	            };
191	            // resource_projection threads the node label (REQ-PICKER-4) — render
192	            // the canonical "LABEL (keyprefix…)" via the one shared helper instead
193	            // of leaking bare key-hex into the picker.
194	            let node_display =
195	                spt_net::net::registry::node_label_display(&row.node, row.node_label.as_deref());
196	            out.push(EndpointRow {
197	                id: row.endpoint_id,
198	                group: format!("{}:{}", s.name, node_display),
199	                node: node_display,
200	                status,
201	                is_local: false,
202	                adapter_profile: row.resources.clone().unwrap_or_default(),
203	                project_history: Vec::new(),
204	                description: row.resources.unwrap_or_default(),
205	                resume_rows: Vec::new(),
206	                // Subnet rows: the registry projection doesn't carry the remote's
207	                // controller marker yet, so leave it unknown — a remote attach
208	                // still busy-refuses with client guidance (REQ-RCVIEW-1).
209	                driven_by: None,
210	                viewer_count: 0,
211	                // Controllability is a LOCAL fact (this node's hosting mode); a
212	                // remote row can't determine it, so it renders plain online/green —
213	                // amber "harness-only" is never inferred across the subnet
214	                // (REQ-PICKER-1). Self-owned rows get reconciled to the live roster
215	                // below (REQ-PICKER-3), which also carries the local controllable.
216	                controllable: None,
217	                endpoint_type: String::new(),
218	            });
219	        }
220	    }
221	    out
222	}
223
224	/// Union an endpoint's ORIGIN project (the cwd it was created in) into its
225	/// committed-context project membership (REQ-PICKER-HISTORY-FRESH). `origin` is
226	/// the cwd-derived `project_id` (`None` when the perch has no recorded/readable
227	/// cwd). A fresh endpoint with no committed context still belongs to the project
228	/// it was created in, so it appears in that project's picker category at once.
229	/// An empty origin (unreadable cwd → empty derivation) or one already present is
230	/// a no-op (dedup); `None` falls back to history-only (no phantom membership).
231	/// Pure over its inputs so the union rule is unit-tested; the git derivation
232	/// itself is covered by `spt_store::project`.
233	// [impl->REQ-PICKER-HISTORY-FRESH]
234	fn merge_origin_project(mut history: Vec<String>, origin: Option<String>) -> Vec<String> {
235	    if let Some(o) = origin {
236	        if !o.is_empty() && !history.contains(&o) {
237	            history.push(o);
238	        }
239	    }
240	    history
241	}
242
243	/// Best-effort project history for a local endpoint, newest→oldest (doyle Q2):
244	/// the projects this endpoint carries context in.
245	///
246	/// The context store is a **git-backed `BranchStore`** (`tracked/.seed.git`):
247	/// per-project context lives on `p-<project>` branches and is only checked out
248	/// to `projects/<project>/<id>/` on demand, so the bare working tree is empty
249	/// and a raw `read_dir` finds nothing for every endpoint (REQ-PICKER-2 — the
250	/// loader bug). Enumerate the branches instead: every `p-<project>` branch whose
251	/// tip tree holds this agent's `<id>/project-context.md`, ordered by tip commit
252	/// recency. Empty if the store is absent/empty — an informational pane,
253	/// degrade-to-empty (any git/IO error → no history, never a crash).
254	// [impl->REQ-PICKER-2]
255	fn project_history_for(id: &str) -> Vec<String> {
256	    use spt_store::contextstore::{self, PROJECT_CONTEXT_FILE};
257	    let git_dir = contextstore::seed_git_dir(&perch::tracked_dir());
258	    if !git_dir.exists() {
259	        return Vec::new();
260	    }
261	    let Ok(store) = spt_store::branchstore::BranchStore::open_or_init(&git_dir) else {
262	        return Vec::new();
263	    };
264	    let Ok(branches) = store.branches_by_recency() else {
265	        return Vec::new();
266	    };
267	    let mut out = Vec::new();
268	    for branch in branches {
269	        // Project branches only (`p-<project_id>`); reverse project_branch().
270	        let Some(project_id) = branch.strip_prefix("p-") else {
271	            continue;
272	        };
273	        // Membership: this agent holds context in the project iff its
274	        // per-(agent,project) file is present in the branch tip tree.
275	        let rel = format!("{id}/{PROJECT_CONTEXT_FILE}");
276	        if matches!(store.read_at_tip(&branch, &rel), Ok(Some(_))) {
277	            out.push(project_id.to_string());
278	        }
279	    }
280	    out
281	}
282
283	/// Resume rows from the per-endpoint session ledger (newest→oldest), titled in
284	/// the model. The ledger row carries no project, so the project field is a
285	/// best-effort fill from the endpoint's most-recent project history; the title
286	/// falls back to the boundary trigger when that is empty.
287	fn resume_rows_for(perch_path: &Path, project_history: &[String]) -> Vec<ResumeRow> {
288	    let project = project_history.first().cloned().unwrap_or_default();
289	    let mut rows: Vec<ResumeRow> = sessions::last_k(perch_path, sessions::MAX_LEDGER)
290	        .into_iter()
291	        .map(|e| ResumeRow {
292	            session_id: e.session_id,
293	            ts: e.ts,
294	            trigger: e.trigger.token().to_string(),
295	            project: project.clone(),
296	        })
297	        .collect();
298	    rows.reverse(); // ledger is oldest→newest; show newest first
299	    rows
300	}
301
302	#[cfg(test)]
303	mod tests {
304	    use super::*;
305
306	    fn row(id: &str, is_local: bool, status: EpStatus) -> EndpointRow {
307	        EndpointRow {
308	            id: id.to_string(),
309	            group: if is_local { "local".into() } else { "sub:n".into() },
310	            node: if is_local { "LOCAL".into() } else { "n".into() },
311	            status,
312	            is_local,
313	            adapter_profile: String::new(),
314	            project_history: Vec::new(),
315	            description: String::new(),
316	            resume_rows: Vec::new(),
317	            driven_by: None,
318	            viewer_count: 0,
319	            controllable: None,
320	            endpoint_type: String::new(),
321	        }
322	    }
323
324	    // [unit->REQ-PICKER-3] a self-owned endpoint dual-listed in Local + Subnet,
325	    // whose stale WAN snapshot disagrees with the live roster, renders the roster
326	    // status in BOTH listings (roster = ground truth); a remote-only row is left
327	    // untouched.
328	    #[test]
329	    fn reconcile_overrides_subnet_with_local_roster() {
330	        let mut local = row("self", true, EpStatus::Online);
331	        local.controllable = Some(true);
332	        local.endpoint_type = "live_agent".into();
333	        // Stale snapshot copy says offline + unknown controllability.
334	        let stale = row("self", false, EpStatus::Offline);
335	        // An endpoint this node does NOT host — must stay as-is.
336	        let remote = row("other", false, EpStatus::Online);
337
338	        let mut rows = vec![local, stale, remote];
339	        reconcile_self_owned(&mut rows);
340
341	        // Both self listings now read the live roster status + controllability.
342	        let self_rows: Vec<&EndpointRow> = rows.iter().filter(|r| r.id == "self").collect();
343	        assert_eq!(self_rows.len(), 2, "both listings kept");
344	        for r in &self_rows {
345	            assert_eq!(r.status, EpStatus::Online, "unified to roster status");
346	            assert_eq!(r.controllable, Some(true));
347	            assert_eq!(r.endpoint_type, "live_agent");
348	        }
349	        // The non-self remote row is untouched.
350	        let other = rows.iter().find(|r| r.id == "other").unwrap();
351	        assert_eq!(other.status, EpStatus::Online);
352	        assert_eq!(other.controllable, None);
353	    }
354
355	    // [unit->REQ-PICKER-HISTORY-FRESH] the origin-project union rule (doyle's 4
356	    // cases): a fresh endpoint belongs to the project it was CREATED in even with
357	    // no committed context, the union dedups, never over-lists a merely-viewed
358	    // project, and degrades safely when the cwd is absent/unreadable.
359	    #[test]
360	    fn origin_project_unions_into_membership() {
361	        // 1. fresh: no committed context, cwd=X ⇒ appears under X (the core fix).
362	        assert_eq!(merge_origin_project(vec![], Some("x".into())), vec!["x"]);
363	        // 2a. committed context in Y + cwd=X ⇒ BOTH (union).
364	        assert_eq!(
365	            merge_origin_project(vec!["y".into()], Some("x".into())),
366	            vec!["y", "x"]
367	        );
368	        // 2b. cwd==committed project ⇒ deduped, not doubled.
369	        assert_eq!(merge_origin_project(vec!["x".into()], Some("x".into())), vec!["x"]);
370	        // 3. cwd=X only ⇒ membership is [X]; a merely-viewed project Z is NOT a
371	        //    member (no over-listing — the filter would never match Z).
372	        assert!(!merge_origin_project(vec![], Some("x".into())).contains(&"z".to_string()));
373	        // 4. cwd None ⇒ history-only, no phantom membership.
374	        assert_eq!(merge_origin_project(vec!["y".into()], None), vec!["y"]);
375	        // 4b. empty origin (unreadable cwd → empty derivation) ⇒ no phantom.
376	        assert_eq!(
377	            merge_origin_project(vec![], Some(String::new())),
378	            Vec::<String>::new()
379	        );
380	    }
381	}
382

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

use std::path::Path;

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

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

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

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

/// The shortcut basename an adapter option declares in its RESOLVED manifest
/// (`[adapter] shortcut_basename`, REQ-MANIFEST-7), defaulting to the
/// harness-agnostic [`DEFAULT_BASENAME`](super::shortcut::DEFAULT_BASENAME)
/// (`spt`) when absent or the adapter is unresolvable. This is the seam that
/// lets spt-claude-code brand its shortcuts `cc-<id>` without spt-core ever
/// hardcoding a harness name.
// [impl->REQ-MANIFEST-7]
pub fn shortcut_basename(adapter_option: &str) -> String {
    let adapters_dir = perch::adapters_dir();
    spt_runtime::registry::resolve_option(&adapters_dir, adapter_option)
        .ok()
        .and_then(|(_, m)| m.adapter.shortcut_basename)
        .filter(|b| !b.trim().is_empty())
        .unwrap_or_else(|| super::shortcut::DEFAULT_BASENAME.to_string())
}

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

/// Reconcile the status of a self-owned endpoint that is dual-listed in both the
/// Local and Subnet categories (REQ-PICKER-3). The two listings draw from
/// independent sources — Local from the live roster (ground truth for an endpoint
/// THIS node hosts), Subnet from the persisted WAN snapshot (a lagged projection)
/// — so a stale snapshot can disagree with the roster and paint the SAME endpoint
/// online in one category and offline in the other (defect 3). The roster wins:
/// for any subnet row whose id matches a local (is_local) row, override the
/// status/controllable/endpoint_type with the local fact. Both listings are kept
/// (distinct views); only the status square is unified.
// [impl->REQ-PICKER-3]
fn reconcile_self_owned(rows: &mut [EndpointRow]) {
    use std::collections::HashMap;
    let local: HashMap<String, (EpStatus, Option<bool>, String)> = rows
        .iter()
        .filter(|r| r.is_local)
        .map(|r| (r.id.clone(), (r.status, r.controllable, r.endpoint_type.clone())))
        .collect();
    for row in rows.iter_mut() {
        if row.is_local {
            continue;
        }
        if let Some((status, controllable, endpoint_type)) = local.get(&row.id) {
            row.status = *status;
            row.controllable = *controllable;
            row.endpoint_type = endpoint_type.clone();
        }
    }
}

/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
fn local_rows() -> Vec<EndpointRow> {
    crate::roster::enumerate()
        .into_iter()
        .map(|p| {
            let perch_path = perch::resolve_perch_path(&p.id, ParentHint::Infer);
            let rec = info::read_info(&perch_path);
            let status = if p.alive {
                EpStatus::Online
            } else {
                EpStatus::Offline
            };
            let adapter_profile = rec
                .as_ref()
                .and_then(|r| r.adapter.clone())
                .unwrap_or_else(|| "(unknown adapter)".to_string());
            let description = rec
                .as_ref()
                .and_then(|r| r.resources.clone())
                .unwrap_or_default();
            // Project membership = committed-context projects (history) UNION the
            // endpoint's ORIGIN project (the cwd it was created in). A FRESH local
            // endpoint has no context branch yet, but it still belongs to the
            // project it was created in — so it appears in that project's picker
            // category immediately (REQ-PICKER-HISTORY-FRESH). Derive the origin
            // exactly like the daemon / project_name() (project_id_for_dir) so the
            // ids match the Project-category filter key.
            let origin_project = rec
                .as_ref()
                .and_then(|r| r.cwd.as_deref())
                .map(|cwd| project::project_id_for_dir(Path::new(cwd)));
            let project_history =
                merge_origin_project(project_history_for(&p.id), origin_project);
            // Resume-from-history is offline + LOCAL only (doyle Q3): only an
            // offline local endpoint reads its ledger.
            let resume_rows = if status == EpStatus::Offline {
                resume_rows_for(&perch_path, &project_history)
            } else {
                Vec::new()
            };
            EndpointRow {
                id: p.id,
                group: "local".to_string(),
                node: "LOCAL".to_string(),
                status,
                is_local: true,
                adapter_profile,
                project_history,
                description,
                resume_rows,
                driven_by: rec.as_ref().and_then(|r| r.driven_by.clone()),
                viewer_count: rec.as_ref().and_then(|r| r.viewer_count).unwrap_or(0),
                // Broker-PTY-controllability + type for the 4-state display
                // (REQ-PICKER-1) — read off this local perch's info.json.
                controllable: rec.as_ref().and_then(|r| r.controllable),
                endpoint_type: rec.as_ref().map(|r| r.state.clone()).unwrap_or_default(),
            }
        })
        .collect()
}

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

    let subnets = spt_store::subnet::SubnetStore::load();
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let mut out = Vec::new();
    for s in &subnets.subnets {
        let Some(reg) = regs.get(&s.name) else {
            continue;
        };
        let hide_new = s.hide_new_endpoints;
        for row in resource_projection(reg, |id| vis.hidden(id, &s.name, hide_new)) {
            let status = match row.status {
                spt_net::net::registry::Status::Active
                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
                // Suspended (cold) + Offline render offline in the W2 duality.
                _ => EpStatus::Offline,
            };
            // resource_projection threads the node label (REQ-PICKER-4) — render
            // the canonical "LABEL (keyprefix…)" via the one shared helper instead
            // of leaking bare key-hex into the picker.
            let node_display =
                spt_net::net::registry::node_label_display(&row.node, row.node_label.as_deref());
            out.push(EndpointRow {
                id: row.endpoint_id,
                group: format!("{}:{}", s.name, node_display),
                node: node_display,
                status,
                is_local: false,
                adapter_profile: row.resources.clone().unwrap_or_default(),
                project_history: Vec::new(),
                description: row.resources.unwrap_or_default(),
                resume_rows: Vec::new(),
                // Subnet rows: the registry projection doesn't carry the remote's
                // controller marker yet, so leave it unknown — a remote attach
                // still busy-refuses with client guidance (REQ-RCVIEW-1).
                driven_by: None,
                viewer_count: 0,
                // Controllability is a LOCAL fact (this node's hosting mode); a
                // remote row can't determine it, so it renders plain online/green —
                // amber "harness-only" is never inferred across the subnet
                // (REQ-PICKER-1). Self-owned rows get reconciled to the live roster
                // below (REQ-PICKER-3), which also carries the local controllable.
                controllable: None,
                endpoint_type: String::new(),
            });
        }
    }
    out
}

/// Union an endpoint's ORIGIN project (the cwd it was created in) into its
/// committed-context project membership (REQ-PICKER-HISTORY-FRESH). `origin` is
/// the cwd-derived `project_id` (`None` when the perch has no recorded/readable
/// cwd). A fresh endpoint with no committed context still belongs to the project
/// it was created in, so it appears in that project's picker category at once.
/// An empty origin (unreadable cwd → empty derivation) or one already present is
/// a no-op (dedup); `None` falls back to history-only (no phantom membership).
/// Pure over its inputs so the union rule is unit-tested; the git derivation
/// itself is covered by `spt_store::project`.
// [impl->REQ-PICKER-HISTORY-FRESH]
fn merge_origin_project(mut history: Vec<String>, origin: Option<String>) -> Vec<String> {
    if let Some(o) = origin {
        if !o.is_empty() && !history.contains(&o) {
            history.push(o);
        }
    }
    history
}

/// Best-effort project history for a local endpoint, newest→oldest (doyle Q2):
/// the projects this endpoint carries context in.
///
/// The context store is a **git-backed `BranchStore`** (`tracked/.seed.git`):
/// per-project context lives on `p-<project>` branches and is only checked out
/// to `projects/<project>/<id>/` on demand, so the bare working tree is empty
/// and a raw `read_dir` finds nothing for every endpoint (REQ-PICKER-2 — the
/// loader bug). Enumerate the branches instead: every `p-<project>` branch whose
/// tip tree holds this agent's `<id>/project-context.md`, ordered by tip commit
/// recency. Empty if the store is absent/empty — an informational pane,
/// degrade-to-empty (any git/IO error → no history, never a crash).
// [impl->REQ-PICKER-2]
fn project_history_for(id: &str) -> Vec<String> {
    use spt_store::contextstore::{self, PROJECT_CONTEXT_FILE};
    let git_dir = contextstore::seed_git_dir(&perch::tracked_dir());
    if !git_dir.exists() {
        return Vec::new();
    }
    let Ok(store) = spt_store::branchstore::BranchStore::open_or_init(&git_dir) else {
        return Vec::new();
    };
    let Ok(branches) = store.branches_by_recency() else {
        return Vec::new();
    };
    let mut out = Vec::new();
    for branch in branches {
        // Project branches only (`p-<project_id>`); reverse project_branch().
        let Some(project_id) = branch.strip_prefix("p-") else {
            continue;
        };
        // Membership: this agent holds context in the project iff its
        // per-(agent,project) file is present in the branch tip tree.
        let rel = format!("{id}/{PROJECT_CONTEXT_FILE}");
        if matches!(store.read_at_tip(&branch, &rel), Ok(Some(_))) {
            out.push(project_id.to_string());
        }
    }
    out
}

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

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

    fn row(id: &str, is_local: bool, status: EpStatus) -> EndpointRow {
        EndpointRow {
            id: id.to_string(),
            group: if is_local { "local".into() } else { "sub:n".into() },
            node: if is_local { "LOCAL".into() } else { "n".into() },
            status,
            is_local,
            adapter_profile: String::new(),
            project_history: Vec::new(),
            description: String::new(),
            resume_rows: Vec::new(),
            driven_by: None,
            viewer_count: 0,
            controllable: None,
            endpoint_type: String::new(),
        }
    }

    // [unit->REQ-PICKER-3] a self-owned endpoint dual-listed in Local + Subnet,
    // whose stale WAN snapshot disagrees with the live roster, renders the roster
    // status in BOTH listings (roster = ground truth); a remote-only row is left
    // untouched.
    #[test]
    fn reconcile_overrides_subnet_with_local_roster() {
        let mut local = row("self", true, EpStatus::Online);
        local.controllable = Some(true);
        local.endpoint_type = "live_agent".into();
        // Stale snapshot copy says offline + unknown controllability.
        let stale = row("self", false, EpStatus::Offline);
        // An endpoint this node does NOT host — must stay as-is.
        let remote = row("other", false, EpStatus::Online);

        let mut rows = vec![local, stale, remote];
        reconcile_self_owned(&mut rows);

        // Both self listings now read the live roster status + controllability.
        let self_rows: Vec<&EndpointRow> = rows.iter().filter(|r| r.id == "self").collect();
        assert_eq!(self_rows.len(), 2, "both listings kept");
        for r in &self_rows {
            assert_eq!(r.status, EpStatus::Online, "unified to roster status");
            assert_eq!(r.controllable, Some(true));
            assert_eq!(r.endpoint_type, "live_agent");
        }
        // The non-self remote row is untouched.
        let other = rows.iter().find(|r| r.id == "other").unwrap();
        assert_eq!(other.status, EpStatus::Online);
        assert_eq!(other.controllable, None);
    }

    // [unit->REQ-PICKER-HISTORY-FRESH] the origin-project union rule (doyle's 4
    // cases): a fresh endpoint belongs to the project it was CREATED in even with
    // no committed context, the union dedups, never over-lists a merely-viewed
    // project, and degrades safely when the cwd is absent/unreadable.
    #[test]
    fn origin_project_unions_into_membership() {
        // 1. fresh: no committed context, cwd=X ⇒ appears under X (the core fix).
        assert_eq!(merge_origin_project(vec![], Some("x".into())), vec!["x"]);
        // 2a. committed context in Y + cwd=X ⇒ BOTH (union).
        assert_eq!(
            merge_origin_project(vec!["y".into()], Some("x".into())),
            vec!["y", "x"]
        );
        // 2b. cwd==committed project ⇒ deduped, not doubled.
        assert_eq!(merge_origin_project(vec!["x".into()], Some("x".into())), vec!["x"]);
        // 3. cwd=X only ⇒ membership is [X]; a merely-viewed project Z is NOT a
        //    member (no over-listing — the filter would never match Z).
        assert!(!merge_origin_project(vec![], Some("x".into())).contains(&"z".to_string()));
        // 4. cwd None ⇒ history-only, no phantom membership.
        assert_eq!(merge_origin_project(vec!["y".into()], None), vec!["y"]);
        // 4b. empty origin (unreadable cwd → empty derivation) ⇒ no phantom.
        assert_eq!(
            merge_origin_project(vec![], Some(String::new())),
            Vec::<String>::new()
        );
    }
}


crates\spt\tests\attach_wedge_e2e.rs:85:/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).
crates\spt\tests\dummy_harness_e2e.rs:178:    // The broker-spawned harness pid (from `ENDPOINT_RUN:… pid=Some(NNN)`) — the
crates\spt\src\cli.rs:1321:                "ENDPOINT_RUN:{id} adapter={adapter} pid={:?} session={session_id} \


driven_by (LOCAL) = raw hex; pid line is consumed by 2 e2e tests. Need those parse sites + node_label_display def before deciding.


170	    while Instant::now() < deadline {
171	        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
172	            online = true;
173	            break;
174	        }
175	        std::thread::sleep(Duration::from_millis(150));
176	    }
177
178	    // The broker-spawned harness pid (from `ENDPOINT_RUN:… pid=Some(NNN)`) — the
179	    // long-lived PTY child itself, NOT the transient `spt api bind` subprocess that
180	    // stamps info.json's pid. It must stay alive (heartbeating).
181	    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
182	    let harness_pid: Option<u32> = run_stderr
183	        .split("pid=Some(")
184	        .nth(1)
185	        .and_then(|s| s.split(')').next())
186	        .and_then(|s| s.trim().parse().ok());
187	    let alive_after = {
188	        std::thread::sleep(Duration::from_millis(700));
189	        harness_pid.map(spt_store::proc::is_process_alive).unwrap_or(false)
190	    };
191
192	    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──
193	    let rc_err = home.path().join("rc.stderr.log");
194	    let rc_err_file = std::fs::File::create(&rc_err).expect("create rc stderr log");

    while Instant::now() < deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
            online = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(150));
    }

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

    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──
    let rc_err = home.path().join("rc.stderr.log");
    let rc_err_file = std::fs::File::create(&rc_err).expect("create rc stderr log");


78	fn sibling_bin(name: &str) -> PathBuf {
79	    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
80	        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
81	}
82
83	/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
84	/// the captured output and the broker-spawned harness pid (parsed from the
85	/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).
86	fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
87	    let mut cmd = Command::new(spt_bin);
88	    cmd.no_window()
89	        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
90	        .env("SPT_HOME", home);
91	    let out = output_bounded(cmd, Duration::from_secs(45));
92	    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
93	    let pid = stderr
94	        .split("pid=Some(")
95	        .nth(1)
96	        .and_then(|s| s.split(')').next())
97	        .and_then(|s| s.trim().parse().ok());

fn sibling_bin(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

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


1	//! Per-subnet eventually-consistent endpoint registry (D3a/b/d/e, ADR-0006).
2	//!
3	//! Maps `endpoint_id → [instance...]` within one subnet, where an [`Instance`] is the
4	//! same logical endpoint as seen running on a particular node, with a liveness
5	//! [`Status`] and the author node's monotonic [`epoch`](Instance::epoch). The same
6	//! `endpoint_id` can hold several instances (the node-global mind advertised from more
7	//! than one machine); picking the right one is the resolution policy's job (D3c) — this
8	//! module owns the **data model + the merge lease**, not resolution.
9	//!
10	//! ## The epoch lease (#8)
11	//!
12	//! Registry updates arrive out of order across a partition or under clock skew. A
13	//! lagging node could re-announce a stale `Active` after the endpoint has actually gone
14	//! `Offline`. [`merge_instance`] defends against this with a **version-vector** rule
15	//! keyed on `(endpoint_id, node)`: an incoming update wins iff its epoch is **strictly
16	//! greater** than the stored one for that node. An equal or lower epoch is dropped as
17	//! stale — so a newer `Offline` (higher epoch) can never be clobbered by a lagging
18	//! `Active` (lower epoch). The epoch is the per-node monotonic counter
19	//! ([`spt_store::epoch`]), NEVER wall-clock. Cross-node *replication* of these updates
20	//! rides the transport (D4); the merge seam is identical for local and wire-delivered
21	//! updates.
22	//!
23	//! ## Join-time bare-id collision check (D3d)
24	//!
25	//! Bare-id uniqueness holds *within* a subnet (ADR-0006 §1): advertising a joining
26	//! endpoint whose id another node already holds in the target subnet refuses via
27	//! [`join_endpoint`](SubnetRegistry::join_endpoint) / [`JoinCollision`] — the clash is
28	//! surfaced and resolved (rename, D3f) before advertisement. Per-subnet: the same bare
29	//! name as *distinct* endpoints in different subnets is legal; the resolver forces
30	//! qualification (D3c).
31	//!
32	//! ## Visibility enforcement (D3e)
33	//!
34	//! Excluded = **not advertised AND not routable** (ADR-0006 §6). The policy lives in
35	//! [`spt_store::visibility::VisibilityStore`]; this module enforces it at both
36	//! registry boundaries: [`advertise_if_visible`] keeps hidden endpoints out of the
37	//! registry entirely, and [`resolve_visible`](SubnetRegistry::resolve_visible) /
38	//! [`resolve_across_visible`] refuse to route to them (even via an explicit `@node`
39	//! pin). Exclusion removes a subnet from the cross-subnet ambiguity set — hidden in
40	//! one subnet, visible in another resolves cleanly in the visible one.

//! Per-subnet eventually-consistent endpoint registry (D3a/b/d/e, ADR-0006).
//!
//! Maps `endpoint_id → [instance...]` within one subnet, where an [`Instance`] is the
//! same logical endpoint as seen running on a particular node, with a liveness
//! [`Status`] and the author node's monotonic [`epoch`](Instance::epoch). The same
//! `endpoint_id` can hold several instances (the node-global mind advertised from more
//! than one machine); picking the right one is the resolution policy's job (D3c) — this
//! module owns the **data model + the merge lease**, not resolution.
//!
//! ## The epoch lease (#8)
//!
//! Registry updates arrive out of order across a partition or under clock skew. A
//! lagging node could re-announce a stale `Active` after the endpoint has actually gone
//! `Offline`. [`merge_instance`] defends against this with a **version-vector** rule
//! keyed on `(endpoint_id, node)`: an incoming update wins iff its epoch is **strictly
//! greater** than the stored one for that node. An equal or lower epoch is dropped as
//! stale — so a newer `Offline` (higher epoch) can never be clobbered by a lagging
//! `Active` (lower epoch). The epoch is the per-node monotonic counter
//! ([`spt_store::epoch`]), NEVER wall-clock. Cross-node *replication* of these updates
//! rides the transport (D4); the merge seam is identical for local and wire-delivered
//! updates.
//!
//! ## Join-time bare-id collision check (D3d)
//!
//! Bare-id uniqueness holds *within* a subnet (ADR-0006 §1): advertising a joining
//! endpoint whose id another node already holds in the target subnet refuses via
//! [`join_endpoint`](SubnetRegistry::join_endpoint) / [`JoinCollision`] — the clash is
//! surfaced and resolved (rename, D3f) before advertisement. Per-subnet: the same bare
//! name as *distinct* endpoints in different subnets is legal; the resolver forces
//! qualification (D3c).
//!
//! ## Visibility enforcement (D3e)
//!
//! Excluded = **not advertised AND not routable** (ADR-0006 §6). The policy lives in
//! [`spt_store::visibility::VisibilityStore`]; this module enforces it at both
//! registry boundaries: [`advertise_if_visible`] keeps hidden endpoints out of the
//! registry entirely, and [`resolve_visible`](SubnetRegistry::resolve_visible) /
//! [`resolve_across_visible`] refuse to route to them (even via an explicit `@node`
//! pin). Exclusion removes a subnet from the cross-subnet ambiguity set — hidden in
//! one subnet, visible in another resolves cleanly in the visible one.


354:pub fn node_label_display(node: &str, label: Option<&str>) -> String {
355-    match label {
356-        Some(l) if !l.trim().is_empty() => format!("{l} ({}…)", key_prefix(node)),
357-        _ => format!("{}…", key_prefix(node)),
358-    }
359-}
360-
361-/// Project one subnet's registry into its resource-registry view, over
362-\ **visible** rows only: `excluded(id)` is the same visibility closure the
363-\ resolver uses ([`SubnetRegistry::resolve_visible`]), so a hidden endpoint
364-/// never appears — discovery leaks nothing a viewer couldn't reach
365-/// (ADR-0006 §6, exclusion by construction). Unroutable (`Offline`) rows are
366-\ skipped too: a dead instance serves nothing, so it is not a resource.
367-// [impl->REQ-INST-14]
368-pub fn resource_projection(
369-    reg: &SubnetRegistry,
370-    excluded: impl Fn(&str) -> bool,
371-) -> Vec<ResourceRow> {
372-    \ Node-level label fill: an instance row carries its authoring node's label
373-    \ ([`Instance::node_label`]), but a row authored before the host stamped one
374-    // (or by an endpoint-less node) may lack it — fall back to the dedicated
375-    // node-label map so the canonical "LABEL (keyprefix…)" render never degrades
376-    // to bare hex when the label IS known (REQ-PICKER-4).
377-    let node_labels: std::collections::BTreeMap<&str, &str> = reg.node_labels().collect();
378-    let mut out = Vec::new();
379-    for id in reg.endpoint_ids() {
380-        if excluded(id) {
381-            continue;
382-        }
383-        for instance in reg.instances(id) {
384-            if !instance.status.routable() {


node_label_display needs a label for driven_by's hex. Find the key→label resolver + how driven_by is stamped.


crates\spt-net\src\net\wanmsg.rs:113:                node_label: None,
crates\spt-net\src\net\replicate.rs:58:/// node still names itself to peers (the per-row [`Instance::node_label`] only
crates\spt-net\src\net\replicate.rs:159:                node_label: None,
crates\spt-net\src\net\replicate.rs:241:    fn feed_carries_instances_and_node_labels_wire_compat() {
crates\spt-net\src\net\registry.rs:127:    pub node_label: Option<String>,
crates\spt-net\src\net\registry.rs:136:    /// [`node_label`](Self::node_label) (the rows are the distribution;
crates\spt-net\src\net\registry.rs:163:/// [`Instance::node_label`] only rides endpoint rows, so an endpoint-less node
crates\spt-net\src\net\registry.rs:185:    node_labels: BTreeMap<String, NodeLabel>,
crates\spt-net\src\net\registry.rs:243:        // forgotten only on an EXPLICIT prune ([`evict_node_labels`], the
crates\spt-net\src\net\registry.rs:255:    pub fn evict_node_labels(&mut self, dead: impl Fn(&str) -> bool) -> usize {
crates\spt-net\src\net\registry.rs:256:        let before = self.node_labels.len();
crates\spt-net\src\net\registry.rs:257:        self.node_labels.retain(|node, _| !dead(node));
crates\spt-net\src\net\registry.rs:258:        before - self.node_labels.len()
crates\spt-net\src\net\registry.rs:265:    pub fn merge_node_label(&mut self, node: &str, label: String, epoch: u64) -> MergeOutcome {
crates\spt-net\src\net\registry.rs:266:        match self.node_labels.get_mut(node) {
crates\spt-net\src\net\registry.rs:276:                self.node_labels
crates\spt-net\src\net\registry.rs:286:    pub fn node_labels(&self) -> impl Iterator<Item = (&str, &str)> {
crates\spt-net\src\net\registry.rs:287:        self.node_labels
crates\spt-net\src\net\registry.rs:338:    /// The authoring node's human-readable label ([`Instance::node_label`],
crates\spt-net\src\net\registry.rs:339:    /// falling back to the registry's [`node_labels`](SubnetRegistry::node_labels)
crates\spt-net\src\net\registry.rs:344:    pub node_label: Option<String>,
crates\spt-net\src\net\registry.rs:354:pub fn node_label_display(node: &str, label: Option<&str>) -> String {
crates\spt-net\src\net\registry.rs:373:    // ([`Instance::node_label`]), but a row authored before the host stamped one
crates\spt-net\src\net\registry.rs:377:    let node_labels: std::collections::BTreeMap<&str, &str> = reg.node_labels().collect();
crates\spt-net\src\net\registry.rs:387:            let node_label = instance
crates\spt-net\src\net\registry.rs:388:                .node_label
crates\spt-net\src\net\registry.rs:390:                .or_else(|| node_labels.get(instance.node.as_str()).map(|l| l.to_string()));
crates\spt-net\src\net\registry.rs:396:                node_label,
crates\spt-net\src\net\registry.rs:630:            .node_label
crates\spt-net\src\net\registry.rs:802:            node_label: None,
crates\spt-net\src\net\registry.rs:958:            node_label: None,
crates\spt-net\src\net\registry.rs:997:    fn node_label_rides_the_lease_and_serde() {
crates\spt-net\src\net\registry.rs:1006:            node_label: label.map(str::to_string),
crates\spt-net\src\net\registry.rs:1017:            reg.instances("ling")[0].node_label.as_deref(),
crates\spt-net\src\net\registry.rs:1025:            reg.instances("ling")[0].node_label.as_deref(),
crates\spt-net\src\net\registry.rs:1035:        assert_eq!(old.node_label, None, "absent field parses as no label");
crates\spt-net\src\net\registry.rs:1047:    // is dropped only on EXPLICIT prune (`evict_node_labels`); and it is
crates\spt-net\src\net\registry.rs:1048:    // additive on the wire (a pre-M8 snapshot with no node_labels parses clean).
crates\spt-net\src\net\registry.rs:1050:    fn node_level_label_lease_persist_prune_and_serde() {
crates\spt-net\src\net\registry.rs:1054:            reg.merge_node_label("n1", "OLDHOST".into(), 1),

[Showing results with pagination = limit: 40]


crates\spt-store\src\info.rs:100:    pub driven_by: Option<String>,
crates\spt-store\src\info.rs:104:    /// add/remove/evict; the picker renders `controlled by <driven_by> (+N
crates\spt-store\src\info.rs:106:    /// best-effort/self-heal caveat as `driven_by`. `default` so older records
crates\spt-store\src\info.rs:192:            driven_by: None,
crates\spt-store\src\info.rs:286:/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):
crates\spt-store\src\info.rs:290:pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
crates\spt-store\src\info.rs:291:    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))
crates\spt-store\src\info.rs:431:            driven_by: None,
crates\spt-store\src\info.rs:505:    // the transient setters (last_active/rest/driven_by/viewer_count — each a
crates\spt-store\src\info.rs:532:                    let _ = set_driven_by(&path, Some("nodehex"));
crates\spt-store\src\info.rs:672:    // [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an
crates\spt-store\src\info.rs:675:    // all) still parses with driven_by == None.
crates\spt-store\src\info.rs:677:    fn set_driven_by_stamps_and_clears() {
crates\spt-store\src\info.rs:686:            !raw.contains("driven_by"),
crates\spt-store\src\info.rs:690:        set_driven_by(d.path(), Some("cafe1234")).unwrap();
crates\spt-store\src\info.rs:692:            read_info(d.path()).unwrap().driven_by.as_deref(),
crates\spt-store\src\info.rs:696:        set_driven_by(d.path(), None).unwrap();
crates\spt-store\src\info.rs:698:            read_info(d.path()).unwrap().driven_by,
crates\spt-store\src\info.rs:705:        assert!(set_driven_by(empty.path(), Some("cafe1234")).is_err());
crates\spt-store\src\info.rs:714:        assert_eq!(read_info(old.path()).unwrap().driven_by, None);
crates\spt-store\src\roster.rs:104:    /// The author's `node_label` lease — strictly-greater-wins. Equal/lower on
crates\spt-store\src\roster.rs:349:    // newer facts; a never-seen pubkey is Inserted. The node_label lease, reused.


driven_by = controller hex. Need label map. Check `endpoint list` label source + self-node label.


1690	    // pump cadence — render the local roster too so a fresh / own endpoint is
1691	    // never missing from the bare list or its `whoami` alias.
1692	    print_local_section();
1693	    0
1694	}
1695
1696	/// Render one subnet's resource rows as an ALIGNED table (REQ-PICKER-5): the
1697	/// node column renders the canonical `LABEL (keyprefix…)`
1698	/// ([`node_label_display`](spt_net::net::registry::node_label_display)) now that
1699	/// [`ResourceRow`](spt_net::net::registry::ResourceRow) carries the label, and
1700	/// every column is max-width padded **by char count** (`…` is multibyte) so a
1701	/// variable-width `endpoint_id` never snaps later fields to a different tab-stop
1702	/// (the old `\t`-separator ragged-column bug). Pure over its inputs;
1703	/// `cmd_endpoint_list` is the impure caller.
1704	// [impl->REQ-PICKER-5]
1705	fn format_subnet_rows(rows: &[spt_net::net::registry::ResourceRow], detail: bool) -> String {
1706	    use spt_net::net::registry::node_label_display;
1707	    // (endpoint_id, node-display, status, resources) pre-rendered cells.
1708	    let cells: Vec<(String, String, String, String)> = rows
1709	        .iter()
1710	        .map(|r| {
1711	            (
1712	                r.endpoint_id.clone(),
1713	                node_label_display(&r.node, r.node_label.as_deref()),
1714	                format!("{:?}", r.status),
1715	                r.resources.clone().unwrap_or_else(|| "-".to_string()),
1716	            )
1717	        })
1718	        .collect();
1719	    let width = |sel: fn(&(String, String, String, String)) -> &String| {
1720	        cells.iter().map(|c| sel(c).chars().count()).max().unwrap_or(0)
1721	    };
1722	    let id_w = width(|c| &c.0);
1723	    let node_w = width(|c| &c.1);
1724	    let status_w = width(|c| &c.2);

    // pump cadence — render the local roster too so a fresh / own endpoint is
    // never missing from the bare list or its `whoami` alias.
    print_local_section();
    0
}

/// Render one subnet's resource rows as an ALIGNED table (REQ-PICKER-5): the
/// node column renders the canonical `LABEL (keyprefix…)`
/// ([`node_label_display`](spt_net::net::registry::node_label_display)) now that
/// [`ResourceRow`](spt_net::net::registry::ResourceRow) carries the label, and
/// every column is max-width padded **by char count** (`…` is multibyte) so a
/// variable-width `endpoint_id` never snaps later fields to a different tab-stop
/// (the old `\t`-separator ragged-column bug). Pure over its inputs;
/// `cmd_endpoint_list` is the impure caller.
// [impl->REQ-PICKER-5]
fn format_subnet_rows(rows: &[spt_net::net::registry::ResourceRow], detail: bool) -> String {
    use spt_net::net::registry::node_label_display;
    // (endpoint_id, node-display, status, resources) pre-rendered cells.
    let cells: Vec<(String, String, String, String)> = rows
        .iter()
        .map(|r| {
            (
                r.endpoint_id.clone(),
                node_label_display(&r.node, r.node_label.as_deref()),
                format!("{:?}", r.status),
                r.resources.clone().unwrap_or_else(|| "-".to_string()),
            )
        })
        .collect();
    let width = |sel: fn(&(String, String, String, String)) -> &String| {
        cells.iter().map(|c| sel(c).chars().count()).max().unwrap_or(0)
    };
    let id_w = width(|c| &c.0);
    let node_w = width(|c| &c.1);
    let status_w = width(|c| &c.2);


crates\spt\src\wansend.rs:505:                    if let Some(label) = &i.node_label {
crates\spt\src\wansend.rs:606:                node_label: None,
crates\spt\src\wansend.rs:918:                    node_label: label.map(str::to_string),
crates\spt\src\picker\data.rs:195:                spt_net::net::registry::node_label_display(&row.node, row.node_label.as_deref());
crates\spt\src\picker\model.rs:69:    pub fn label(self) -> &'static str {
crates\spt\src\picker\model.rs:178:    pub fn label(self, project_name: &str) -> String {
crates\spt\src\cli.rs:860:        /// The dead identity: pubkey hex, unambiguous prefix, or label.
crates\spt\src\cli.rs:1713:                node_label_display(&r.node, r.node_label.as_deref()),
crates\spt\src\cli.rs:2670:/// [`roster::detect_self_id`]), never the `--from` reply label: a hosted agent's
crates\spt\src\cli.rs:2781:    // identity ([`roster::detect_self_id`]), never the `--from` reply label.
crates\spt\src\cli.rs:3938:                e.0 = inst.node_label.clone();
crates\spt\src\cli.rs:4473:            eprintln!("NO_MATCH:{node} — no member identity matches (hex, prefix, or label)");
crates\spt\src\cli.rs:4579:            [] => eprintln!("NO_MATCH:{node} — no member identity matches (hex, prefix, or label)"),
crates\spt\src\cli.rs:7430:                node_label: Some("HFENDULEAM".into()),
crates\spt\src\cli.rs:7437:                node_label: None,
crates\spt\src\cli.rs:9757:                node_label: None,
crates\spt\src\cli.rs:10138:                    node_label: None,
crates\spt\src\cli.rs:10212:            node_label: label.map(str::to_string),
crates\spt\src\cli.rs:10315:        reg.merge_node_label("idle0bb", "HFENDULEAM".into(), 1);
crates\spt\src\cli.rs:10317:        reg.merge_node_label("ghost0cc", "GHOST".into(), 1);


Reconcile item 2: model already yields `Attach` for create+resume; only offline `Start` skips attach. Need the original §W4 wording.


Bash

grep -n -A40 "W4" V0.13.0-DELIVERY-CONTROL-JIT.md 2>/dev/null | head -70

Read W4 section of delivery-control JIT


187:### W4 — Picker UX (todlando build, doyle gate)
188-- Skip the first screen → open directly on **Pick existing**; `n` jumps to Create new.
189-- **Auto-attach** after Start-new AND Resume-from-history (both currently don't attach + show no
190-  stdout); add an `h` shortcut to run headless (no attach).
191-- `controlled by` shows the **node NAME** (node_label_display), not the raw hex (D).
192-- Clean up the Start-new output: drop the Rust `pid=Some(142748)` leak + the "harness binds its
193-  perch on startup" internals; user-friendly, not process-log.
194-
195-### W5 — `driven_by` self-heal (todlando build, doyle gate)
196-- `ONLINE+CONTROLLED` must clear even if a detach is lost (don't rely on the detach IPC — same
197-  lesson as B2/REQ-HAZARD-HOSTED-LIVENESS-RECONCILE): reconcile clears `driven_by` when the
198-  endpoint has no live controller/session. Composes with W1 (wedge no longer blocks detach).
199-
200-### W6 — perri hand-off (doyle, after W2)
201-- Direct perri to the UPDATED integration checklist + docs to build the spt-claude-code
202-  **translation binary** (CC choreography: ctrl+s→50ms→payload+\r→50ms→ctrl+s). Per the standing
203-  route: doyle designs (W0 ADR) → perri builds the adapter binary → doyle gates → perri validates.
204-
205-### Side task (do early, docs-only) — correct v0.12.1 changelog
206-- v0.12.1 notes claim "a freshly-created agent appears under its own project right away" — REFUTED
207-  (P1 never worked). Amend CHANGELOG.md (main) + `gh release edit v0.12.1` notes, no retag
208-  (v0.12.0-precedent). v0.13.0 delivers it for real (W3).
209-
210-## Sequencing notes
211-- W0 gates W2 (design before build). W1 (wedge/control) is independent + the scariest — can start
212:  immediately (repro-first). W3/W4/W5 are independent bug/UX fixes, parallelizable. W2+W6 are the
213-  feature spine. The folded v0.12.2 work (window fix @15fdf58, MD-sweep @2435a74, ATX @b3bb387)
214-  rides the v0.13.0 release; its changelog bullets merge in.
215-- Version: **v0.13.0** (MINOR — new delivery feature + breaking-ish behavior). Confirm at release.


Now the legend (item 2 needs `h` added) + `spawned.pid` type.


33:/// The pinned keybind legend (bottom row). Designed so a future tap-mode can
35:const LEGEND: &str = "←→ category · ↑↓ item · enter confirm · esc back · / filter · n new · s shortcut";
119:    f.render_widget(legend(), chunks[1]);
125:        Line::from("Enter an endpoint id. Alphanumeric + hyphen + underscore only."),
170:fn legend() -> Paragraph<'static> {
178:/// else the pinned keybind legend.
185:        None => legend(),
195:            Constraint::Length(1), // filter / legend
249:    // Bottom: the filter line (when filtering) else the legend.
258:        f.render_widget(legend(), rows[2]);
402:    f.render_widget(legend(), chunks[1]);


46:/// handle) plus the OS pid of the spawned harness (`None` if the broker backend
49:pub struct HarnessSpawned {
51:    pub pid: Option<u32>,
137:pub struct PreparedSpawn {
174:    let (session_id, pid) = brain
175:        .spawn_session_pid(SpawnReq {
186:    Ok(HarnessSpawned { session_id, pid })


`spawned.pid: Option<u32>`. Legend is one global const. Read full view.rs to place `h` hint + confirm render.