1	---
2	name: m12-w1-progress
3	description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
8	---
9
10	M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).
11
12	**Built + green:**
13	- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
14	- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
15	- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
16	- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).
17
18	**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.
19
20	**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.
21
22	**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).
23
24	**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).
25
26	**W1.5 GATE PASS (doyle 2026-06-14, independently verified — read shared helper + both int tests + reproduced trace).** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).
27
28	**REQ-HOST-RUN-2 GATE PASS (doyle 2026-06-14) — verified end-to-end.** doyle: "the confirmed-missing gating foundation is DONE + gated; the hard part of M12 is done." CONTINUE-vs-CHECKPOINT (W2-W5) routed to OPERATOR; HOLD building until operator relays. doyle suggested order if continue: **W3 first** (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code) → then W2/W4/W5. Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.
29
30	**COMMITTED 0c687fe (2026-06-14): W1.5 + REQ-HOST-RUN-2** on m12-w1-bringup-rc (operator chose commit-then-continue; 24 files, 668+/92-). W1.x bringup+rc foundation COMPLETE + gated (spt-hosted bringup + cross-node + LOCAL rc + project cwd).
31
32	**W3 GATE PASS (doyle 2026-06-14, independently verified — reproduced traceable + read every load-bearing test; praised read-time containment re-apply, local-pointer provenance, unchanged schema).** UNCOMMITTED (operator calls commit, same as W1.5+HR2 pattern). doyle design ruling = `M12-W3-RULING.md`; plan = `M12-W3-PLAN.md`. REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered, traceable EXIT=0 both [OK]. **T3.1** file-backed `[strings]`: `profile::as_file_pointer` = table with EXACTLY one key `file` (else opaque — back-compat); `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copy_dir_all at register, Pointer reads live); HAZARD containment (reject `..`/absolute → register REFUSES loud via `RegistryError::BadStringPointer`, records nothing); read-time skip-diag (eprintln STRINGS_FILE_SKIP → None, never crash/error); local-profile pointers resolve to user-owned profiles dir (update-safe). **T3.2**: resolve_option ALREADY adapter:profile-aware → contract-lock unit + CONTEXT.md:137 reconcile, NO config field added (no consumer; escalation clause untriggered). EVIDENCE: spt-runtime 49/0 (6 new tests); full spt-runtime+spt-daemon+spt+spt-term suite **576/0**; clippy EXIT=0; traceable EXIT=0. manifest.schema.json UNCHANGED (strings opaque Value). UNCOMMITTED on m12-w1-bringup-rc.
33
34	**Remaining M12 after W3 gate:** W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).
35

---
name: m12-w1-progress
description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
metadata:
  node_type: memory
  type: project
  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
---

M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).

**Built + green:**
- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).

**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.

**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.

**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).

**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).

**W1.5 GATE PASS (doyle 2026-06-14, independently verified — read shared helper + both int tests + reproduced trace).** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

**REQ-HOST-RUN-2 GATE PASS (doyle 2026-06-14) — verified end-to-end.** doyle: "the confirmed-missing gating foundation is DONE + gated; the hard part of M12 is done." CONTINUE-vs-CHECKPOINT (W2-W5) routed to OPERATOR; HOLD building until operator relays. doyle suggested order if continue: **W3 first** (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code) → then W2/W4/W5. Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.

**COMMITTED 0c687fe (2026-06-14): W1.5 + REQ-HOST-RUN-2** on m12-w1-bringup-rc (operator chose commit-then-continue; 24 files, 668+/92-). W1.x bringup+rc foundation COMPLETE + gated (spt-hosted bringup + cross-node + LOCAL rc + project cwd).

**W3 GATE PASS (doyle 2026-06-14, independently verified — reproduced traceable + read every load-bearing test; praised read-time containment re-apply, local-pointer provenance, unchanged schema).** UNCOMMITTED (operator calls commit, same as W1.5+HR2 pattern). doyle design ruling = `M12-W3-RULING.md`; plan = `M12-W3-PLAN.md`. REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered, traceable EXIT=0 both [OK]. **T3.1** file-backed `[strings]`: `profile::as_file_pointer` = table with EXACTLY one key `file` (else opaque — back-compat); `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copy_dir_all at register, Pointer reads live); HAZARD containment (reject `..`/absolute → register REFUSES loud via `RegistryError::BadStringPointer`, records nothing); read-time skip-diag (eprintln STRINGS_FILE_SKIP → None, never crash/error); local-profile pointers resolve to user-owned profiles dir (update-safe). **T3.2**: resolve_option ALREADY adapter:profile-aware → contract-lock unit + CONTEXT.md:137 reconcile, NO config field added (no consumer; escalation clause untriggered). EVIDENCE: spt-runtime 49/0 (6 new tests); full spt-runtime+spt-daemon+spt+spt-term suite **576/0**; clippy EXIT=0; traceable EXIT=0. manifest.schema.json UNCHANGED (strings opaque Value). UNCOMMITTED on m12-w1-bringup-rc.

**Remaining M12 after W3 gate:** W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).


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


Edit

**Remaining M12 after W3 gate:** W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).

**W2 BUILT + COMMITTED @6e76f62 (2026-06-14) — pending doyle gate.** Interactive `spt endpoint run` ratatui picker. doyle design ruling = `M12-W2-RULING.md`; plan = `M12-W2-PLAN.md` (crystallized). Operator authorized incremental commits ("commit as needed, history tidy") — committed pre-gate; doyle gates the committed state. In-process `spt/src/picker/` (model/data/view/shortcut/mod), action-enum single source of truth. **SINGLE-BRINGUP-PATH invariant** (doyle's gate condition): every terminal action → Outcome enum → dispatch routes Run→cmd_endpoint_run, Attach→rc::run_attach; ChangeAdapter/Instantiate/Fork = screen re-entry into create-new (still cmd_endpoint_run), NOT a 2nd spawn. Bare `spt endpoint run` (adapter/id → Option) = picker; flagged = untouched non-interactive path; additive `--create`. Deps ratatui 0.29 + nucleo-matcher 0.3. **doyle rulings absorbed:** Q1 attached blue ■ + Kick → **W2.5** (own broker attach-presence+force-detach slice AFTER W2, own JIT+gate; forward-pointer in plan + docs/DEFERRED.md); Q3 Resume-from-history BUILT (sessions::last_k ledger exists M10/REQ-TERM-6, offline+LOCAL); Q4 rev.2 shortcut basename = PARAMETER default `spt`→`spt-<id>` (adapter overrides→cc; spt-core NEVER bakes cc; .cmd not .ps1 PATHEXT; sentinel-guard); Q5 additive --create. REQ-RUN-PICKER + REQ-RUN-SHORTCUT (doc+impl+unit, NO int — Buffer+state-model unit asserts, live key loop = manual-verify per REQ-PAIR-6). EVIDENCE: 26 picker unit tests, full workspace suite green, clippy -D EXIT=0, traceable 200/200 both [OK].

**Remaining M12 after W2 gate:** W2.5 (attached-presence+Kick, scheduled) → W4 subnet QR → W5 whoami. Commit per operator. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).


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

**W2 BUILT + COMMITTED @6e76f62 (2026-06-14) — pending doyle gate.** Interactive `spt endpoint run` ratatui picker. doyle design ruling = `M12-W2-RULING.md`; plan = `M12-W2-PLAN.md` (crystallized). Operator authorized incremental commits ("commit as needed, history tidy") — committed pre-gate; doyle gates the committed state. In-process `spt/src/picker/` (model/data/view/shortcut/mod), action-enum single source of truth. **SINGLE-BRINGUP-PATH invariant** (doyle's gate condition): every terminal action → Outcome enum → dispatch routes Run→cmd_endpoint_run, Attach→rc::run_attach; ChangeAdapter/Instantiate/Fork = screen re-entry into create-new (still cmd_endpoint_run), NOT a 2nd spawn. Bare `spt endpoint run` (adapter/id → Option) = picker; flagged = untouched non-interactive path; additive `--create`. Deps ratatui 0.29 + nucleo-matcher 0.3. **doyle rulings absorbed:** Q1 attached blue ■ + Kick → **W2.5** (own broker attach-presence+force-detach slice AFTER W2, own JIT+gate; forward-pointer in plan + docs/DEFERRED.md); Q3 Resume-from-history BUILT (sessions::last_k ledger exists M10/REQ-TERM-6, offline+LOCAL); Q4 rev.2 shortcut basename = PARAMETER default `spt`→`spt-<id>` (adapter overrides→cc; spt-core NEVER bakes cc; .cmd not .ps1 PATHEXT; sentinel-guard); Q5 additive --create. REQ-RUN-PICKER + REQ-RUN-SHORTCUT (doc+impl+unit, NO int — Buffer+state-model unit asserts, live key loop = manual-verify per REQ-PAIR-6). EVIDENCE: 26 picker unit tests, full workspace suite green, clippy -D EXIT=0, traceable 200/200 both [OK].

**Remaining M12 after W2 gate:** W2.5 (attached-presence+Kick, scheduled) → W4 subnet QR → W5 whoami. Commit per operator. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).

---
name: m12-w1-progress
description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
metadata:
  node_type: memory
  type: project
  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
---

M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).

**Built + green:**
- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).

**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.

**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.

**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).

**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).

**W1.5 GATE PASS (doyle 2026-06-14, independently verified — read shared helper + both int tests + reproduced trace).** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

**REQ-HOST-RUN-2 GATE PASS (doyle 2026-06-14) — verified end-to-end.** doyle: "the confirmed-missing gating foundation is DONE + gated; the hard part of M12 is done." CONTINUE-vs-CHECKPOINT (W2-W5) routed to OPERATOR; HOLD building until operator relays. doyle suggested order if continue: **W3 first** (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code) → then W2/W4/W5. Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.

**COMMITTED 0c687fe (2026-06-14): W1.5 + REQ-HOST-RUN-2** on m12-w1-bringup-rc (operator chose commit-then-continue; 24 files, 668+/92-). W1.x bringup+rc foundation COMPLETE + gated (spt-hosted bringup + cross-node + LOCAL rc + project cwd).

**W3 GATE PASS (doyle 2026-06-14, independently verified — reproduced traceable + read every load-bearing test; praised read-time containment re-apply, local-pointer provenance, unchanged schema).** UNCOMMITTED (operator calls commit, same as W1.5+HR2 pattern). doyle design ruling = `M12-W3-RULING.md`; plan = `M12-W3-PLAN.md`. REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered, traceable EXIT=0 both [OK]. **T3.1** file-backed `[strings]`: `profile::as_file_pointer` = table with EXACTLY one key `file` (else opaque — back-compat); `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copy_dir_all at register, Pointer reads live); HAZARD containment (reject `..`/absolute → register REFUSES loud via `RegistryError::BadStringPointer`, records nothing); read-time skip-diag (eprintln STRINGS_FILE_SKIP → None, never crash/error); local-profile pointers resolve to user-owned profiles dir (update-safe). **T3.2**: resolve_option ALREADY adapter:profile-aware → contract-lock unit + CONTEXT.md:137 reconcile, NO config field added (no consumer; escalation clause untriggered). EVIDENCE: spt-runtime 49/0 (6 new tests); full spt-runtime+spt-daemon+spt+spt-term suite **576/0**; clippy EXIT=0; traceable EXIT=0. manifest.schema.json UNCHANGED (strings opaque Value). UNCOMMITTED on m12-w1-bringup-rc.

**Remaining M12 after W3 gate:** W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).

-**Remaining M12 after W3 gate:** W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).

+**W2 BUILT + COMMITTED @6e76f62 (2026-06-14) — pending doyle gate.** Interactive `spt endpoint run` ratatui picker. doyle design ruling = `M12-W2-RULING.md`; plan = `M12-W2-PLAN.md` (crystallized). Operator authorized incremental commits ("commit as needed, history tidy") — committed pre-gate; doyle gates the committed state. In-process `spt/src/picker/` (model/data/view/shortcut/mod), action-enum single source of truth. **SINGLE-BRINGUP-PATH invariant** (doyle's gate condition): every terminal action → Outcome enum → dispatch routes Run→cmd_endpoint_run, Attach→rc::run_attach; ChangeAdapter/Instantiate/Fork = screen re-entry into create-new (still cmd_endpoint_run), NOT a 2nd spawn. Bare `spt endpoint run` (adapter/id → Option) = picker; flagged = untouched non-interactive path; additive `--create`. Deps ratatui 0.29 + nucleo-matcher 0.3. **doyle rulings absorbed:** Q1 attached blue ■ + Kick → **W2.5** (own broker attach-presence+force-detach slice AFTER W2, own JIT+gate; forward-pointer in plan + docs/DEFERRED.md); Q3 Resume-from-history BUILT (sessions::last_k ledger exists M10/REQ-TERM-6, offline+LOCAL); Q4 rev.2 shortcut basename = PARAMETER default `spt`→`spt-<id>` (adapter overrides→cc; spt-core NEVER bakes cc; .cmd not .ps1 PATHEXT; sentinel-guard); Q5 additive --create. REQ-RUN-PICKER + REQ-RUN-SHORTCUT (doc+impl+unit, NO int — Buffer+state-model unit asserts, live key loop = manual-verify per REQ-PAIR-6). EVIDENCE: 26 picker unit tests, full workspace suite green, clippy -D EXIT=0, traceable 200/200 both [OK].

+**Remaining M12 after W2 gate:** W2.5 (attached-presence+Kick, scheduled) → W4 subnet QR → W5 whoami. Commit per operator. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).


Updated task #9 status


W2 done. Summary:

**Plan → verified → executed → committed.**

**JIT plan** (`M12-W2-PLAN.md`) — W1.5/W3 template. doyle design-checked, ruled in `M12-W2-RULING.md` (verified source first). Rulings + 2 mid-build operator corrections absorbed:
- **Q1** attached blue ■ + Kick → **W2.5** (own broker attach-presence + force-detach slice, AFTER W2, own JIT+gate). Forward-pointer in plan + `docs/DEFERRED.md`.
- **Q3** Resume-from-history **BUILT** (doyle caught my un-run TODO — `sessions::last_k` ledger exists; offline+LOCAL).
- **Q4** (rev.2) shortcut basename = **parameter** default `spt`→`spt-<id>` (adapter overrides→`cc`; spt-core never bakes `cc`); `.cmd` not `.ps1` (PATHEXT); sentinel-guard; bare-`spt` shadow rejected.
- **Q5** additive `--create`.

**Built** `spt/src/picker/` (in-process, action-enum single source of truth):
- `model.rs` pure state/transitions · `data.rs` read-only gather over existing surfaces · `view.rs` ratatui Buffer render · `shortcut.rs` `<basename>-<id>` launcher · `mod.rs` crossterm live loop + dispatch
- Bare `spt endpoint run` → picker; flagged form untouched. `cmd_endpoint_run` pub(crate). Deps ratatui 0.29 + nucleo-matcher 0.3.
- **Gate invariant held:** no second bringup path — every Outcome routes through `cmd_endpoint_run`/`rc::run_attach`; re-entrant options are screen transitions back into create-new.

**Sweep green:** 26 picker unit tests (state-model + Buffer asserts), full workspace suite green, clippy `-D` EXIT=0, traceable **200/200** (REQ-RUN-PICKER + REQ-RUN-SHORTCUT doc+impl+unit, no int — live loop = manual-verify per REQ-PAIR-6).

**Committed @6e76f62** (operator authorized history-tidy commits). Pinged doyle to gate the committed state.

Waiting on doyle's gate. Then W2.5 (scheduled) → W4 → W5 → full M12 gate (unblocks perri on spt-claude-code).


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="d202d620" type="msg" from="doyle">W2 GATE: PASS ✓ (doyle, independently verified @6e76f62)

Reproduced myself:
- traceable-reqs check: 200/200, 0 findings; REQ-RUN-PICKER + REQ-RUN-SHORTCUT both [doc+impl+unit].
- cargo test -p spt --bins: 151 passed/0. Load-bearing green on my machine: resume_outcome_bakes_session, basename_is_parameteriz</EVENT-PART>
<EVENT-PART seq="2/7" id="d202d620">ed, resume_buffer_lists_sessions, bakes_resume_and_action, resume_title_render.

BINDING INVARIANT (single bringup path) — VERIFIED two ways:
- grep picker/ for spawn|launch_harness|establish_perch|net_dial|brokered (minus cmd_endpoint_run/run_attach) = EMPTY.
- READ dispatch (mod.rs:247): only Run→cli::cmd_endpoint_run, Attach</EVENT-PART>
<EVENT-PART seq="3/7" id="d202d620">→rc::run_attach; Shortcut/Cancelled→0 no spawn. Re-entrant ChangeAdapter/Instantiate/Fork = reenter_create SCREEN transitions, converge back on cmd_endpoint_run. No second path. Invariant holds.

Spot-reads all faithful:
- resume_outcome (model.rs:621) bakes selected_resume().session_id → Run{resume:Some}; reads sessions::last_k</EVENT-PART>
<EVENT-PART seq="4/7" id="d202d620">. No new daemon surface. Offline+LOCAL gated. ✓
- shortcut.rs: basename param (DEFAULT_BASENAME=&quot;spt&quot;, never cc; adapter overrides), .cmd-not-.ps1 w/ PATHEXT rationale, invocation-reality header, SENTINEL-guarded overwrite (Created/Updated/RefusedForeign — refuses foreign file), terminal-actions-only. Every Q4 ruling landed. </EVENT-PART>
<EVENT-PART seq="5/7" id="d202d620">✓
- Buffer tests via TestBackend assert squares/tree/resume. Live key loop = manual-verify leg (noted, accepted). ✓

W2 CLEARED.

ONE FOLLOW-ON (operator idea, ruled AFTER your build — NOT in @6e76f62): bare `spt` (no subcommand) → alias for `spt endpoint run` (this picker), replacing print-help-on-no-args. Removes any n</EVENT-PART>
<EVENT-PART seq="6/7" id="d202d620">eed for a cwd wrapper; `spt` = muscle-memory every project. MANDATORY TTY-GUARD: interactive TTY → picker; NON-TTY (pipe/CI/redirect) → fall back to today's help, NEVER launch the TUI (else scripts hang). `spt --help`/`-h`/`spt help` always print help regardless. Folds into REQ-RUN-PICKER + a unit test (non-TTY bare → help, no picker). S</EVENT-PART>
<EVENT-PART seq="7/7" id="d202d620">mall — do it as a quick W2 follow-on, ping me to gate, THEN W4 subnet QR. Full detail in M12-W2-RULING.md &quot;Bare spt → picker&quot;.

Nice work — clean wave.</EVENT-PART></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.


W2 GATE: **PASS ✓** (doyle independently verified — invariant checked two ways, all spot-reads faithful).

One follow-on before W4: bare `spt` → picker, with mandatory TTY-guard (non-TTY → help). ACK + implement.


Grep

struct Cli|#\[command|command: Option|Parser\)\]|fn run\(|Cli::parse|arg_required_else_help|print_help|no.?args


crates\spt\src\cli.rs:41:#[derive(Parser)]
crates\spt\src\cli.rs:42:#[command(
crates\spt\src\cli.rs:50:struct Cli {
crates\spt\src\cli.rs:51:    #[command(subcommand)]
crates\spt\src\cli.rs:115:        #[command(subcommand)]
crates\spt\src\cli.rs:122:    #[command(name = "how-to")]
crates\spt\src\cli.rs:134:        #[command(subcommand)]
crates\spt\src\cli.rs:157:        #[command(subcommand)]
crates\spt\src\cli.rs:166:        #[command(subcommand)]
crates\spt\src\cli.rs:176:        #[command(subcommand)]
crates\spt\src\cli.rs:184:        #[command(subcommand)]
crates\spt\src\cli.rs:193:        #[command(subcommand)]
crates\spt\src\cli.rs:206:        #[command(subcommand)]
crates\spt\src\cli.rs:359:        #[command(subcommand)]
crates\spt\src\cli.rs:367:        #[command(subcommand)]
crates\spt\src\cli.rs:434:    #[command(hide = true)]
crates\spt\src\cli.rs:878:pub fn run() -> i32 {
crates\spt\src\api\mod.rs:45:    #[command(subcommand)]
crates\spt\src\api\mod.rs:89:        #[command(flatten)]
crates\spt\src\api\mod.rs:108:        #[command(flatten)]
crates\spt\src\api\mod.rs:116:        #[command(flatten)]
crates\spt\src\api\mod.rs:130:        #[command(flatten)]
crates\spt\src\api\mod.rs:137:        #[command(flatten)]
crates\spt\src\api\mod.rs:143:        #[command(flatten)]
crates\spt\src\api\mod.rs:149:        #[command(flatten)]
crates\spt\src\api\mod.rs:160:        #[command(flatten)]
crates\spt\src\api\mod.rs:168:        #[command(flatten)]
crates\spt\src\api\mod.rs:174:        #[command(flatten)]
crates\spt\src\api\mod.rs:180:        #[command(flatten)]
crates\spt\src\api\mod.rs:186:        #[command(flatten)]

[Showing results with pagination = limit: 30]


41	#[derive(Parser)]
42	#[command(
43	    name = "spt",
44	    version,
45	    about = "spt — a harness-independent core for an agent ecosystem: inter-agent \
46	             messaging, live-agent lifecycle, terminal hosting, P2P networking, \
47	             seamless self-update. Docs: https://sabermage.github.io/spt-releases",
48	    long_about = None
49	)]
50	struct Cli {
51	    #[command(subcommand)]
52	    cmd: Cmd,
53	}
54
55	#[derive(Subcommand)]
56	enum Cmd {
57	    /// Send a message (body read from stdin); fire-and-forget.
58	    Send {
59	        /// Target perch id. May be omitted when --reply-to is given.
60	        target: Option<String>,
61	        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
62	        #[arg(long)]
63	        from: Option<String>,
64	        /// Reply to this sender; sets the target and labels output REPLIED.
65	        #[arg(long)]
66	        reply_to: Option<String>,
67	        /// Spool only (deferred / hook channel) — no live wake.
68	        #[arg(long)]
69	        deferred: bool,
70	        /// Request the `user-msg` type (the user's authority). Honored only from
71	        /// a user-backed origin (a Gateway endpoint, or the local user's own
72	        /// CLI); an agent-family sender is re-stamped to plain `msg` (REQ-MSG-5).
73	        #[arg(long = "user-msg")]
74	        user_msg: bool,
75	    },
76	    /// Send and block for a reply (body read from stdin).
77	    ///
78	    /// The reply body is printed to stdout; gives up after --timeout seconds.
79	    Ring {
80	        /// Target perch id.
81	        target: String,
82	        /// Sender id (auto-detected from session if omitted).
83	        #[arg(long)]
84	        from: Option<String>,
85	        /// Seconds to wait for a reply before giving up.
86	        #[arg(long, default_value = "60")]
87	        timeout: u64,
88	    },
89	    /// Become reachable: register the perch and listen (blocks).
90	    ///
91	    /// Drains the spooled backlog first; each received message prints to
92	    /// stdout. With --once, runs a single drain+receive cycle and exits.
93	    Ready {
94	        /// This agent's perch id.
95	        id: String,
96	        /// Run a single drain+receive cycle, then exit (one-shot fallback
97	        /// for harnesses that cannot host a long-running listener).
98	        #[arg(long)]
99	        once: bool,
100	        /// Home subnet for a NEW endpoint (required on a multi-subnet

#[derive(Parser)]
#[command(
    name = "spt",
    version,
    about = "spt — a harness-independent core for an agent ecosystem: inter-agent \
             messaging, live-agent lifecycle, terminal hosting, P2P networking, \
             seamless self-update. Docs: https://sabermage.github.io/spt-releases",
    long_about = None
)]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// Send a message (body read from stdin); fire-and-forget.
    Send {
        /// Target perch id. May be omitted when --reply-to is given.
        target: Option<String>,
        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
        /// Reply to this sender; sets the target and labels output REPLIED.
        #[arg(long)]
        reply_to: Option<String>,
        /// Spool only (deferred / hook channel) — no live wake.
        #[arg(long)]
        deferred: bool,
        /// Request the `user-msg` type (the user's authority). Honored only from
        /// a user-backed origin (a Gateway endpoint, or the local user's own
        /// CLI); an agent-family sender is re-stamped to plain `msg` (REQ-MSG-5).
        #[arg(long = "user-msg")]
        user_msg: bool,
    },
    /// Send and block for a reply (body read from stdin).
    ///
    /// The reply body is printed to stdout; gives up after --timeout seconds.
    Ring {
        /// Target perch id.
        target: String,
        /// Sender id (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,
        /// Seconds to wait for a reply before giving up.
        #[arg(long, default_value = "60")]
        timeout: u64,
    },
    /// Become reachable: register the perch and listen (blocks).
    ///
    /// Drains the spooled backlog first; each received message prints to
    /// stdout. With --once, runs a single drain+receive cycle and exits.
    Ready {
        /// This agent's perch id.
        id: String,
        /// Run a single drain+receive cycle, then exit (one-shot fallback
        /// for harnesses that cannot host a long-running listener).
        #[arg(long)]
        once: bool,
        /// Home subnet for a NEW endpoint (required on a multi-subnet


`cmd: Cmd` is required. Read `run()` dispatch entry:


878	pub fn run() -> i32 {
879	    use clap::{CommandFactory, FromArgMatches};
880	    let mut cmd = Cli::command();
881	    let template = grouped_help_template(&mut cmd);
882	    let matches = cmd.help_template(template).get_matches();
883	    let cli = match Cli::from_arg_matches(&matches) {
884	        Ok(cli) => cli,
885	        Err(e) => e.exit(),
886	    };
887	    match cli.cmd {
888	        Cmd::Send {
889	            target,
890	            from,
891	            reply_to,
892	            deferred,
893	            user_msg,
894	        } => cmd_send(target, from, reply_to, deferred, user_msg),
895	        Cmd::Ring {
896	            target,
897	            from,
898	            timeout,
899	        } => cmd_ring(target, from, timeout),
900	        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
901	        Cmd::Whoami => cmd_whoami(),
902	        Cmd::Subnet { action } => {
903	            match action {
904	                // Bare `spt subnet` = the flagless status view WITH the hint
905	                // footer; explicit `status` drops it (M8 decision 12).
906	                None => cmd_subnet_status(None, false, true),
907	                Some(SubnetCmd::Status { name, nodes }) => cmd_subnet_status(name, nodes, false),
908	                Some(SubnetCmd::Create { name }) => cmd_subnet_create(name),
909	                Some(SubnetCmd::ShowCode { name }) => cmd_subnet_show_code(name),
910	                Some(SubnetCmd::Join { name, code }) => cmd_subnet_join(name, code),
911	                Some(SubnetCmd::Detach { name, save }) => cmd_subnet_attachment(&name, true, save),
912	                Some(SubnetCmd::Attach { name, save }) => cmd_subnet_attachment(&name, false, save),
913	                Some(SubnetCmd::Leave { name }) => cmd_subnet_leave(&name),
914	                Some(SubnetCmd::Prune { node }) => cmd_subnet_prune(&node),
915	                Some(SubnetCmd::Revoke {
916	                    nodes,
917	                    force_rotate_seed,
918	                }) => cmd_subnet_revoke(&nodes, force_rotate_seed),
919	                // notify's M8 home (decision 4); the hot path above stays
920	                // flat — moved verbs have no top-level shims (decision 9).
921	                // [impl->REQ-CLI-3]
922	                Some(SubnetCmd::Notify { body, target, from }) => cmd_notify(body, target, from),

pub fn run() -> i32 {
    use clap::{CommandFactory, FromArgMatches};
    let mut cmd = Cli::command();
    let template = grouped_help_template(&mut cmd);
    let matches = cmd.help_template(template).get_matches();
    let cli = match Cli::from_arg_matches(&matches) {
        Ok(cli) => cli,
        Err(e) => e.exit(),
    };
    match cli.cmd {
        Cmd::Send {
            target,
            from,
            reply_to,
            deferred,
            user_msg,
        } => cmd_send(target, from, reply_to, deferred, user_msg),
        Cmd::Ring {
            target,
            from,
            timeout,
        } => cmd_ring(target, from, timeout),
        Cmd::Ready { id, once, subnet } => run_listen(&id, once, subnet.as_deref()),
        Cmd::Whoami => cmd_whoami(),
        Cmd::Subnet { action } => {
            match action {
                // Bare `spt subnet` = the flagless status view WITH the hint
                // footer; explicit `status` drops it (M8 decision 12).
                None => cmd_subnet_status(None, false, true),
                Some(SubnetCmd::Status { name, nodes }) => cmd_subnet_status(name, nodes, false),
                Some(SubnetCmd::Create { name }) => cmd_subnet_create(name),
                Some(SubnetCmd::ShowCode { name }) => cmd_subnet_show_code(name),
                Some(SubnetCmd::Join { name, code }) => cmd_subnet_join(name, code),
                Some(SubnetCmd::Detach { name, save }) => cmd_subnet_attachment(&name, true, save),
                Some(SubnetCmd::Attach { name, save }) => cmd_subnet_attachment(&name, false, save),
                Some(SubnetCmd::Leave { name }) => cmd_subnet_leave(&name),
                Some(SubnetCmd::Prune { node }) => cmd_subnet_prune(&node),
                Some(SubnetCmd::Revoke {
                    nodes,
                    force_rotate_seed,
                }) => cmd_subnet_revoke(&nodes, force_rotate_seed),
                // notify's M8 home (decision 4); the hot path above stays
                // flat — moved verbs have no top-level shims (decision 9).
                // [impl->REQ-CLI-3]
                Some(SubnetCmd::Notify { body, target, from }) => cmd_notify(body, target, from),


1	---
2	name: m12-w1-progress
3	description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
8	---
9
10	M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).
11
12	**Built + green:**
13	- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
14	- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
15	- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
16	- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).
17
18	**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.
19
20	**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.
21
22	**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).
23
24	**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).
25
26	**W1.5 GATE PASS (doyle 2026-06-14, independently verified — read shared helper + both int tests + reproduced trace).** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).
27
28	**REQ-HOST-RUN-2 GATE PASS (doyle 2026-06-14) — verified end-to-end.** doyle: "the confirmed-missing gating foundation is DONE + gated; the hard part of M12 is done." CONTINUE-vs-CHECKPOINT (W2-W5) routed to OPERATOR; HOLD building until operator relays. doyle suggested order if continue: **W3 first** (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code) → then W2/W4/W5. Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.
29
30	**COMMITTED 0c687fe (2026-06-14): W1.5 + REQ-HOST-RUN-2** on m12-w1-bringup-rc (operator chose commit-then-continue; 24 files, 668+/92-). W1.x bringup+rc foundation COMPLETE + gated (spt-hosted bringup + cross-node + LOCAL rc + project cwd).
31
32	**W3 GATE PASS (doyle 2026-06-14, independently verified — reproduced traceable + read every load-bearing test; praised read-time containment re-apply, local-pointer provenance, unchanged schema).** UNCOMMITTED (operator calls commit, same as W1.5+HR2 pattern). doyle design ruling = `M12-W3-RULING.md`; plan = `M12-W3-PLAN.md`. REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered, traceable EXIT=0 both [OK]. **T3.1** file-backed `[strings]`: `profile::as_file_pointer` = table with EXACTLY one key `file` (else opaque — back-compat); `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copy_dir_all at register, Pointer reads live); HAZARD containment (reject `..`/absolute → register REFUSES loud via `RegistryError::BadStringPointer`, records nothing); read-time skip-diag (eprintln STRINGS_FILE_SKIP → None, never crash/error); local-profile pointers resolve to user-owned profiles dir (update-safe). **T3.2**: resolve_option ALREADY adapter:profile-aware → contract-lock unit + CONTEXT.md:137 reconcile, NO config field added (no consumer; escalation clause untriggered). EVIDENCE: spt-runtime 49/0 (6 new tests); full spt-runtime+spt-daemon+spt+spt-term suite **576/0**; clippy EXIT=0; traceable EXIT=0. manifest.schema.json UNCHANGED (strings opaque Value). UNCOMMITTED on m12-w1-bringup-rc.
33

... [3 lines truncated] ...


W2 cleared. Implementing bare-`spt`→picker with TTY-guard (testable decision seam). Check `grouped_help_template`:


836:fn grouped_help_template(cmd: &mut clap::Command) -> String {
837-    cmd.build(); // materialize the implicit `help` subcommand
838-    let (mut user, mut agent) = (Vec::new(), Vec::new());
839-    for sc in cmd.get_subcommands() {
840-        if sc.is_hide_set() {
841-            continue;
842-        }


Edit

struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

struct Cli {
    // Optional so a BARE `spt` (no subcommand) parses to `None` instead of a
    // clap "required subcommand" error — bare `spt` opens the endpoint-run
    // picker on an interactive terminal (REQ-RUN-PICKER). `--help`/`-h`/`help`
    // are handled by clap before we ever see `None`.
    #[command(subcommand)]
    cmd: Option<Cmd>,
}


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

struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

struct Cli {
    // Optional so a BARE `spt` (no subcommand) parses to `None` instead of a
    // clap "required subcommand" error — bare `spt` opens the endpoint-run
    // picker on an interactive terminal (REQ-RUN-PICKER). `--help`/`-h`/`help`
    // are handled by clap before we ever see `None`.
    #[command(subcommand)]
    cmd: Option<Cmd>,
}

+    // Optional so a BARE `spt` (no subcommand) parses to `None` instead of a

+    // clap "required subcommand" error — bare `spt` opens the endpoint-run

+    // picker on an interactive terminal (REQ-RUN-PICKER). `--help`/`-h`/`help`

+    // are handled by clap before we ever see `None`.


    let mut cmd = Cli::command();
    let template = grouped_help_template(&mut cmd);
    let matches = cmd.help_template(template).get_matches();
    let cli = match Cli::from_arg_matches(&matches) {
        Ok(cli) => cli,
        Err(e) => e.exit(),
    };
    match cli.cmd {
        Cmd::Send {

    let mut cmd = Cli::command();
    let template = grouped_help_template(&mut cmd);
    let matches = cmd.help_template(template.clone()).get_matches();
    let cli = match Cli::from_arg_matches(&matches) {
        Ok(cli) => cli,
        Err(e) => e.exit(),
    };
    // Bare `spt` (no subcommand): an interactive terminal opens the endpoint-run
    // picker; a non-TTY (pipe / redirect / CI) NEVER launches the TUI (it would
    // hang a script) — fall back to help. `--help`/`-h`/`help` already printed
    // help and exited inside clap above.
    let Some(command) = cli.cmd else {
        return bare_invocation(template);
    };
    match command {
        Cmd::Send {


    let mut cmd = Cli::command();
    let template = grouped_help_template(&mut cmd);
    let matches = cmd.help_template(template).get_matches();
    let cli = match Cli::from_arg_matches(&matches) {
        Ok(cli) => cli,
        Err(e) => e.exit(),
    };
    match cli.cmd {
        Cmd::Send {

    let mut cmd = Cli::command();
    let template = grouped_help_template(&mut cmd);
    let matches = cmd.help_template(template.clone()).get_matches();
    let cli = match Cli::from_arg_matches(&matches) {
        Ok(cli) => cli,
        Err(e) => e.exit(),
    };
    // Bare `spt` (no subcommand): an interactive terminal opens the endpoint-run
    // picker; a non-TTY (pipe / redirect / CI) NEVER launches the TUI (it would
    // hang a script) — fall back to help. `--help`/`-h`/`help` already printed
    // help and exited inside clap above.
    let Some(command) = cli.cmd else {
        return bare_invocation(template);
    };
    match command {
        Cmd::Send {

-    let matches = cmd.help_template(template).get_matches();

+    let matches = cmd.help_template(template.clone()).get_matches();

+    // Bare `spt` (no subcommand): an interactive terminal opens the endpoint-run

+    // picker; a non-TTY (pipe / redirect / CI) NEVER launches the TUI (it would

+    // hang a script) — fall back to help. `--help`/`-h`/`help` already printed

+    // help and exited inside clap above.

+        return bare_invocation(template);


Add the bare-invocation logic + testable decision seam before `grouped_help_template`:


fn grouped_help_template(cmd: &mut clap::Command) -> String {

/// What a bare `spt` (no subcommand) should do, decided purely from terminal
/// state so it is unit-testable without a real TTY.
#[derive(Debug, PartialEq, Eq)]
enum BareAction {
    /// Interactive terminal → open the endpoint-run picker.
    Picker,
    /// Non-TTY (pipe / redirect / CI) → print help, never launch the TUI.
    Help,
}

/// The TTY-guard: the picker needs BOTH a readable key source and a renderable
/// screen, so it opens only when stdin AND stdout are terminals. Anything else
/// (a pipe, a redirect, a CI runner) falls back to help — launching a TUI there
/// would hang a script.
// [impl->REQ-RUN-PICKER]
fn decide_bare(stdin_tty: bool, stdout_tty: bool) -> BareAction {
    if stdin_tty && stdout_tty {
        BareAction::Picker
    } else {
        BareAction::Help
    }
}

/// Bare `spt`: open the picker on an interactive terminal, else print help.
// [impl->REQ-RUN-PICKER]
fn bare_invocation(template: String) -> i32 {
    use clap::CommandFactory;
    use std::io::IsTerminal;
    match decide_bare(std::io::stdin().is_terminal(), std::io::stdout().is_terminal()) {
        BareAction::Picker => crate::picker::run(None, None),
        BareAction::Help => {
            let mut cmd = Cli::command();
            let _ = cmd.help_template(template).print_help();
            println!();
            0
        }
    }
}

fn grouped_help_template(cmd: &mut clap::Command) -> String {


/// What a bare `spt` (no subcommand) should do, decided purely from terminal
/// state so it is unit-testable without a real TTY.
#[derive(Debug, PartialEq, Eq)]
enum BareAction {
    /// Interactive terminal → open the endpoint-run picker.
    Picker,
    /// Non-TTY (pipe / redirect / CI) → print help, never launch the TUI.
    Help,
}

/// The TTY-guard: the picker needs BOTH a readable key source and a renderable
/// screen, so it opens only when stdin AND stdout are terminals. Anything else
/// (a pipe, a redirect, a CI runner) falls back to help — launching a TUI there
/// would hang a script.
// [impl->REQ-RUN-PICKER]
fn decide_bare(stdin_tty: bool, stdout_tty: bool) -> BareAction {
    if stdin_tty && stdout_tty {
        BareAction::Picker
    } else {
        BareAction::Help
    }
}

/// Bare `spt`: open the picker on an interactive terminal, else print help.
// [impl->REQ-RUN-PICKER]
fn bare_invocation(template: String) -> i32 {
    use clap::CommandFactory;
    use std::io::IsTerminal;
    match decide_bare(std::io::stdin().is_terminal(), std::io::stdout().is_terminal()) {
        BareAction::Picker => crate::picker::run(None, None),
        BareAction::Help => {
            let mut cmd = Cli::command();
            let _ = cmd.help_template(template).print_help();
            println!();
            0
        }
    }
}

fn grouped_help_template(cmd: &mut clap::Command) -> String {

/// `Commands:` block replaced by two alphabetized groups (user-facing first,

/// agent-facing second). Entries are generated from the derived `Command`

/// itself, so abouts can never drift from the list.

+/// What a bare `spt` (no subcommand) should do, decided purely from terminal

+/// state so it is unit-testable without a real TTY.

+    /// Interactive terminal → open the endpoint-run picker.

+    /// Non-TTY (pipe / redirect / CI) → print help, never launch the TUI.

+/// The TTY-guard: the picker needs BOTH a readable key source and a renderable

+/// screen, so it opens only when stdin AND stdout are terminals. Anything else

+/// (a pipe, a redirect, a CI runner) falls back to help — launching a TUI there

+fn decide_bare(stdin_tty: bool, stdout_tty: bool) -> BareAction {

+/// Bare `spt`: open the picker on an interactive terminal, else print help.

+fn bare_invocation(template: String) -> i32 {

+    match decide_bare(std::io::stdin().is_terminal(), std::io::stdout().is_terminal()) {

+        BareAction::Picker => crate::picker::run(None, None),

+            let mut cmd = Cli::command();

+            let _ = cmd.help_template(template).print_help();

cmd.build(); // materialize the implicit `help` subcommand

let (mut user, mut agent) = (Vec::new(), Vec::new());


Add unit test for the TTY-guard decision. Find cli.rs test module:


1007:            EndpointCmd::Run {
5918:#[cfg(test)]
5919:mod tests {


5918	#[cfg(test)]
5919	mod tests {
5920	    use super::*;
5921
5922	    fn parse(args: &[&str]) -> Result<Cli, clap::Error> {
5923	        Cli::try_parse_from(args)
5924	    }
5925
5926	    // [unit->REQ-MSG-5] the LOCAL origination gate: a bare CLI (no agent
5927	    // session) is the user-backed local user → user-msg honored (a composed
5928	    // envelope); a hosted agent's session re-stamps to a plain msg body
5929	    // (loudly); a Gateway-typed perch is user-backed → honored. A non-request
5930	    // and an empty body never compose and never report a re-stamp.
5931	    #[test]
5932	    fn apply_user_msg_gate_local_origination() {
5933	        use spt_proto::event::parse_event;
5934	        let _h = crate::testutil::isolated_home();
5935
5936	        // Bare local user: no session id → LocalUserCli → honored.
5937	        let (body, restamped) = apply_user_msg_gate(None, "me", "ship it", true);
5938	        assert!(!restamped);
5939	        let p = parse_event(&body).expect("composed a user-msg envelope");
5940	        assert_eq!(p.event_type.as_deref(), Some("user-msg"));
5941	        assert_eq!(p.from(), Some("me"));
5942	        assert_eq!(p.body, "ship it");
5943
5944	        // A hosted agent perch → agent-family → re-stamped to a plain body.
5945	        let agent = "doyle";
5946	        let perch_path = perch::resolve_perch_path(agent, ParentHint::Infer);
5947	        std::fs::create_dir_all(&perch_path).unwrap();

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

    fn parse(args: &[&str]) -> Result<Cli, clap::Error> {
        Cli::try_parse_from(args)
    }

    // [unit->REQ-MSG-5] the LOCAL origination gate: a bare CLI (no agent
    // session) is the user-backed local user → user-msg honored (a composed
    // envelope); a hosted agent's session re-stamps to a plain msg body
    // (loudly); a Gateway-typed perch is user-backed → honored. A non-request
    // and an empty body never compose and never report a re-stamp.
    #[test]
    fn apply_user_msg_gate_local_origination() {
        use spt_proto::event::parse_event;
        let _h = crate::testutil::isolated_home();

        // Bare local user: no session id → LocalUserCli → honored.
        let (body, restamped) = apply_user_msg_gate(None, "me", "ship it", true);
        assert!(!restamped);
        let p = parse_event(&body).expect("composed a user-msg envelope");
        assert_eq!(p.event_type.as_deref(), Some("user-msg"));
        assert_eq!(p.from(), Some("me"));
        assert_eq!(p.body, "ship it");

        // A hosted agent perch → agent-family → re-stamped to a plain body.
        let agent = "doyle";
        let perch_path = perch::resolve_perch_path(agent, ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();


    fn parse(args: &[&str]) -> Result<Cli, clap::Error> {
        Cli::try_parse_from(args)
    }

    fn parse(args: &[&str]) -> Result<Cli, clap::Error> {
        Cli::try_parse_from(args)
    }

    // [unit->REQ-RUN-PICKER] bare `spt` (no subcommand) parses to `None` (it no
    // longer errors as a missing required subcommand) so the runner can route it
    // to the picker / help.
    #[test]
    fn bare_spt_parses_to_none() {
        let cli = parse(&["spt"]).expect("bare spt parses");
        assert!(cli.cmd.is_none());
        // a real subcommand still parses to Some.
        assert!(parse(&["spt", "whoami"]).unwrap().cmd.is_some());
    }

    // [unit->REQ-RUN-PICKER] the TTY-guard: bare `spt` opens the picker ONLY with
    // an interactive stdin+stdout; any non-TTY (pipe / redirect / CI) falls back
    // to help and NEVER launches the TUI (which would hang a script).
    #[test]
    fn bare_tty_guard() {
        assert_eq!(decide_bare(true, true), BareAction::Picker);
        assert_eq!(decide_bare(false, true), BareAction::Help, "piped stdin → help");
        assert_eq!(decide_bare(true, false), BareAction::Help, "redirected stdout → help");
        assert_eq!(decide_bare(false, false), BareAction::Help, "CI/non-tty → help");
    }


    fn parse(args: &[&str]) -> Result<Cli, clap::Error> {
        Cli::try_parse_from(args)
    }

    // [unit->REQ-RUN-PICKER] bare `spt` (no subcommand) parses to `None` (it no
    // longer errors as a missing required subcommand) so the runner can route it
    // to the picker / help.
    #[test]
    fn bare_spt_parses_to_none() {
        let cli = parse(&["spt"]).expect("bare spt parses");
        assert!(cli.cmd.is_none());
        // a real subcommand still parses to Some.
        assert!(parse(&["spt", "whoami"]).unwrap().cmd.is_some());
    }

    // [unit->REQ-RUN-PICKER] the TTY-guard: bare `spt` opens the picker ONLY with
    // an interactive stdin+stdout; any non-TTY (pipe / redirect / CI) falls back
    // to help and NEVER launches the TUI (which would hang a script).
    #[test]
    fn bare_tty_guard() {
        assert_eq!(decide_bare(true, true), BareAction::Picker);
        assert_eq!(decide_bare(false, true), BareAction::Help, "piped stdin → help");
        assert_eq!(decide_bare(true, false), BareAction::Help, "redirected stdout → help");
        assert_eq!(decide_bare(false, false), BareAction::Help, "CI/non-tty → help");
    }

+    // [unit->REQ-RUN-PICKER] bare `spt` (no subcommand) parses to `None` (it no

+    // longer errors as a missing required subcommand) so the runner can route it

+        let cli = parse(&["spt"]).expect("bare spt parses");

+        // a real subcommand still parses to Some.

+        assert!(parse(&["spt", "whoami"]).unwrap().cmd.is_some());

+    // [unit->REQ-RUN-PICKER] the TTY-guard: bare `spt` opens the picker ONLY with

+    // an interactive stdin+stdout; any non-TTY (pipe / redirect / CI) falls back

+    // to help and NEVER launches the TUI (which would hang a script).

+        assert_eq!(decide_bare(true, true), BareAction::Picker);

+        assert_eq!(decide_bare(false, true), BareAction::Help, "piped stdin → help");

+        assert_eq!(decide_bare(true, false), BareAction::Help, "redirected stdout → help");

+        assert_eq!(decide_bare(false, false), BareAction::Help, "CI/non-tty → help");

// [unit->REQ-MSG-5] the LOCAL origination gate: a bare CLI (no agent

// session) is the user-backed local user → user-msg honored (a composed

// envelope); a hosted agent's session re-stamps to a plain msg body


Build + run cli/picker tests:


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && cargo build -p spt 2>&1 | grep -iE 'error|warning' | head; echo "BUILD_DONE"; cargo test -p spt --bin spt 'tests::bare' 2>&1 | grep -iE 'test result|FAILED|bare'; echo "---picker---"; cargo test -p spt --bin spt picker:: 2>&1 | grep -iE 'test result'


<owl_messages>
<owl_message from="doyle" priority="high">NEW SMALL M12 ITEM (operator) — shortcut basename must become a MANIFEST element, + doc the adapter checklist.

STATUS I verified: today basename is NOT a manifest element — mod.rs:233 hardcodes DEFAULT_BASENAME="spt" into write_shortcut; the param exists but the picker never varies it; no manifest field (grep manifest.rs + MANIFEST.md empty). So spt-claude-code CANNOT actually emit cc-<id> yet — it'd get spt-<id>. The "adapter overrides" path doesn't exist; it's only a code default. Gap.

FIX (lands in M12 — adapter-facing contract must be in the PUBLISHED surface before perri builds spt-claude-code):
1. Add optional manifest field `[adapter] shortcut_basename` (default "spt"). Additive + N-1-safe; regen manifest.schema.json (ADR-0001, CI drift-gated).
2. Picker reads the RESOLVED manifest for the selected adapter → pass its shortcut_basename to write_shortcut instead of the hardcoded const. Fallback to "spt" when absent.
3. DOCS (the operator's ask — "update the adapter integration checklist"): docs/MANIFEST.md `[adapter]` section gets the new optional field; the `claude-spt` worked example (MANIFEST.md:240) shows `shortcut_basename = "cc"`. harness-contract docs-site regenerates from it.
4. Unit: manifest-declared basename overrides the default (adapter with shortcut_basename="cc" → cc-<id>; absent → spt-<id>). Keep the existing basename_is_parameterized API test.

REQ: new REQ-MANIFEST-7 (adapter-declared shortcut basename) OR extend REQ-RUN-SHORTCUT — your granularity call (MANIFEST-* is the manifest-element convention, slight lean there). doc+impl+unit.

Sequence: small — do it WITH the bare-spt→picker follow-on, before W4. Ping me to gate both together. Build uncommitted (operator calls commit; W2 was a one-off pre-auth).</owl_message>
</owl_messages>

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


warning: variable does not need to be mutable
warning: `spt` (bin "spt") generated 1 warning (run `cargo fix --bin "spt" -p spt` to apply 1 suggestion)
BUILD_DONE
---picker---


W2 gate PASS noted. New high-priority item — ACK first.


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

1) W2 GATE PASS — thanks, clean verify (single-bringup-path grep + dispatch read + spot-reads). Logged.

2) shortcut_basename → manifest element. You're right, real gap: today it's a code-default only, the override path doesn't exist, so spt-claude-code would get spt-<id>. Adapter-facing contract MUST be in the published surface before perri. Doing it NOW with the bare-spt→picker follow-on:
   - manifest [adapter] optional `shortcut_basename` (default "spt"); additive N-1-safe; regen manifest.schema.json (ADR-0001 drift-gate).
   - picker resolves the SELECTED adapter's manifest → its shortcut_basename → write_shortcut (fallback "spt"); kills the hardcoded const at the call site (DEFAULT_BASENAME stays as the absent-fallback).
   - docs/MANIFEST.md [adapter] section + claude-spt worked example `shortcut_basename = "cc"`; docs-site regenerates.
   - REQ: minting **REQ-MANIFEST-7** (manifest-element convention, your lean) doc+impl+unit. Unit: declared "cc"→cc-<id>, absent→spt-<id>; keep basename_is_parameterized API test.

Bare-spt→picker already built (Cli.cmd→Option, TTY-guard decide_bare: stdin&&stdout tty→picker else help; --help/-h/help untouched; 2 unit tests). UNCOMMITTED this batch — operator calls commit (W2 pre-auth was one-off, understood). Will ping you to gate bare-spt + REQ-MANIFEST-7 together before W4.
EOF
echo "EXIT=$?"


➜ SENT:doyle
EXIT=0


Fix the `mut` warning + investigate manifest Adapter struct, schema regen, MANIFEST.md:


Edit

        BareAction::Help => {
            let mut cmd = Cli::command();
            let _ = cmd.help_template(template).print_help();

        BareAction::Help => {
            let cmd = Cli::command();
            let _ = cmd.help_template(template).print_help();


        BareAction::Help => {
            let mut cmd = Cli::command();
            let _ = cmd.help_template(template).print_help();

        BareAction::Help => {
            let cmd = Cli::command();
            let _ = cmd.help_template(template).print_help();

match decide_bare(std::io::stdin().is_terminal(), std::io::stdout().is_terminal()) {

BareAction::Picker => crate::picker::run(None, None),

-            let mut cmd = Cli::command();


Grep

pub struct Adapter\b|pub struct AdapterSection|kind: AdapterKind|adapter_name|min_spt_core_version|pub struct.*\{|\[adapter\]


26:/// `docs/MANIFEST.md` tables; all but `[adapter]` are optional so a minimal
29:pub struct Manifest {
58:    /// `adapter_name` ignores these (parent unmodified); the composite
85:/// `[adapter]` — the manifest header, readable before any update (compat gate).
87:pub struct Adapter {
90:    pub kind: AdapterKind,
93:    pub min_spt_core_version: String,
112:pub struct Hook {
127:pub struct Hint {
165:pub struct Session {
234:pub struct SessionRole {
255:pub struct EnvVar {
274:pub struct History {
321:pub struct Digest {
344:pub struct Inject {
362:pub struct Identity {
380:pub struct Update {
406:    /// Verify spt-core satisfies `min_spt_core_version` before/after.
428:pub struct Shell {
476:pub struct ShellCapability {
483:pub struct Sensory {
749:[adapter]
752:min_spt_core_version = "1.0.0"
756:fires = "api seed --pid {parent_pid} --session-id {session_id} --adapter {adapter_name}"
807:[adapter]

[Showing results with pagination = limit: 25]


Found 11 files
crates\spt\src\picker\shortcut.rs
crates\spt-runtime\src\manifest.rs
crates\spt-runtime\src\lib.rs
crates\spt-daemon\src\brainproc.rs
crates\spt-daemon\src\relcache.rs
crates\spt-daemon\src\propagate.rs
crates\xtask\src\main.rs
crates\spt-daemon\src\release.rs
crates\spt-store\src\nodeid.rs
crates\spt-net\src\net\update.rs
crates\spt-daemon\src\machineid.rs


85	/// `[adapter]` — the manifest header, readable before any update (compat gate).
86	#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
87	pub struct Adapter {
88	    pub name: String,
89	    #[serde(default)]
90	    pub kind: AdapterKind,
91	    pub version: String,
92	    /// Lowest spt-core version this adapter tolerates (compat gate).
93	    pub min_spt_core_version: String,
94	    /// Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).
95	    #[serde(default, skip_serializing_if = "Vec::is_empty")]
96	    pub hostable_types: Vec<String>,
97	}
98
99	/// The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven
100	/// surface (MANIFEST §Shell adapters).
101	#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
102	#[serde(rename_all = "snake_case")]
103	pub enum AdapterKind {
104	    #[default]
105	    Harness,
106	    Shell,
107	}
108
109	/// `[hooks.<event>]` — one harness event → the `api` command it fires, the
110	/// stdin fields it maps, and whether it can surface context to the agent.
111	#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
112	pub struct Hook {
113	    /// Opaque `api …` command line the harness invokes for this event.
114	    pub fires: String,

/// `[adapter]` — the manifest header, readable before any update (compat gate).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Adapter {
    pub name: String,
    #[serde(default)]
    pub kind: AdapterKind,
    pub version: String,
    /// Lowest spt-core version this adapter tolerates (compat gate).
    pub min_spt_core_version: String,
    /// Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub hostable_types: Vec<String>,
}

/// The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven
/// surface (MANIFEST §Shell adapters).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AdapterKind {
    #[default]
    Harness,
    Shell,
}

/// `[hooks.<event>]` — one harness event → the `api` command it fires, the
/// stdin fields it maps, and whether it can surface context to the agent.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Hook {
    /// Opaque `api …` command line the harness invokes for this event.
    pub fires: String,


9://!   llms-full.txt / manifest.schema.json / install scripts at site root +
202:            "llms-full.txt" | "manifest.schema.json" => true,
288:            "crates/spt-runtime/manifest.schema.json",
289:            "manifest.schema.json",


`Adapter` derives `JsonSchema` (schema auto-generated). Find the schema-gen/check seam:


crates\spt-runtime\Cargo.toml:17:schemars = "1"
crates\spt-runtime\manifest.schema.json:761:  "$id": "https://sabermage.github.io/spt-releases/manifest.schema.json"
crates\spt-runtime\src\manifest.rs:20:use schemars::JsonSchema;
crates\spt-runtime\src\manifest.rs:64:    #[schemars(with = "BTreeMap<String, serde_json::Value>")]
crates\spt-runtime\src\manifest.rs:74:    #[schemars(with = "BTreeMap<String, serde_json::Value>")]
crates\spt-runtime\src\manifest.rs:588:/// (`crates/spt-runtime/manifest.schema.json`) is the site/release asset;
crates\spt-runtime\src\manifest.rs:598:pub fn manifest_json_schema() -> String {
crates\spt-runtime\src\manifest.rs:599:    let schema = schemars::schema_for!(Manifest);
crates\spt-runtime\src\manifest.rs:604:        "https://sabermage.github.io/spt-releases/manifest.schema.json".into(),
crates\spt-runtime\src\manifest.rs:1174:    fn manifest_schema_shape() {
crates\spt-runtime\src\manifest.rs:1179:            "https://sabermage.github.io/spt-releases/manifest.schema.json"
crates\spt-runtime\src\manifest.rs:1203:    /// The drift gate (M6-D3): the checked-in `manifest.schema.json` — the
crates\spt-runtime\src\manifest.rs:1210:    fn checked_in_schema_is_current() {
crates\spt-runtime\src\manifest.rs:1212:        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("manifest.schema.json");
crates\spt-runtime\src\manifest.rs:1214:            std::fs::write(&path, &want).expect("bless schema");
crates\spt-runtime\src\manifest.rs:1218:            "checked-in manifest.schema.json (regenerate: SPT_BLESS=1 cargo test -p spt-runtime)",
crates\spt-runtime\src\manifest.rs:1223:            "manifest.schema.json drifted from the derives — regenerate with \


Schema is derived + drift-gated (`SPT_BLESS=1 cargo test -p spt-runtime`). Add the manifest field:


Edit

    /// Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub hostable_types: Vec<String>,
}

    /// Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub hostable_types: Vec<String>,
    /// Optional basename for the `spt endpoint run` picker's `<basename>-<id>`
    /// launcher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default
    /// `spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts
    /// (spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted
    /// from serialization when absent). The picker reads it from the RESOLVED
    /// manifest of the selected adapter.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub shortcut_basename: Option<String>,
}


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

    /// Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub hostable_types: Vec<String>,
}

    /// Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub hostable_types: Vec<String>,
    /// Optional basename for the `spt endpoint run` picker's `<basename>-<id>`
    /// launcher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default
    /// `spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts
    /// (spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted
    /// from serialization when absent). The picker reads it from the RESOLVED
    /// manifest of the selected adapter.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub shortcut_basename: Option<String>,
}

+    /// Optional basename for the `spt endpoint run` picker's `<basename>-<id>`

+    /// launcher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default

+    /// `spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts

+    /// (spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted

+    /// from serialization when absent). The picker reads it from the RESOLVED

+    /// manifest of the selected adapter.

+    #[serde(default, skip_serializing_if = "Option::is_none")]

+    pub shortcut_basename: Option<String>,

/// The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven


Find `Adapter {` literals needing the new field:


crates\spt\src\cli.rs:169:    Adapter {
crates\spt\src\cli.rs:1041:        Cmd::Adapter { action } => cmd_adapter(action),
crates\spt\src\cli.rs:6564:            Cmd::Adapter {
crates\spt\src\cli.rs:6575:            Cmd::Adapter {
crates\spt\src\cli.rs:6586:            Cmd::Adapter {
crates\spt\src\cli.rs:6596:            Cmd::Adapter {
crates\spt\src\cli.rs:6604:            Cmd::Adapter {
crates\spt\src\cli.rs:6614:            Cmd::Adapter {
crates\spt\src\cli.rs:6622:            Cmd::Adapter {
crates\spt-runtime\src\manifest.rs:87:pub struct Adapter {


Only `Cmd::Adapter` (CLI enum, unrelated) + the struct def — no manifest `Adapter{}` literals (deserialized). Build spt-runtime + regen schema:


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && cargo build -p spt-runtime 2>&1 | grep -iE 'error|warning' | head; echo "BUILD"; SPT_BLESS=1 cargo test -p spt-runtime checked_in_schema_is_current 2>&1 | grep -iE 'test result|bless|FAILED'; echo "BLESS_DONE"


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


Schema blessed. Add manifest unit test — check existing test pattern:


1174	        assert!(matches!(e, ManifestError::Parse(_)));
1175	    }
1176
1177	    // ---- M6-D3: the published manifest JSON Schema ----
1178
1179	    #[test] // [unit->REQ-DOCS-4] the generated schema carries the stable $id at
1180	            // the canonical Pages URL (ADR-0014), the adapter header's requireds, and
1181	            // the closed enum vocabularies (the things adapter authors validate against).
1182	    fn manifest_schema_shape() {
1183	        let s = manifest_json_schema();
1184	        let v: serde_json::Value = serde_json::from_str(&s).expect("schema is valid JSON");
1185	        assert_eq!(
1186	            v["$id"],
1187	            "https://sabermage.github.io/spt-releases/manifest.schema.json"
1188	        );
1189	        assert_eq!(v["title"], "spt-core runtime manifest");
1190	        // [adapter] is the only required top-level section.
1191	        let req: Vec<&str> = v["required"]
1192	            .as_array()
1193	            .unwrap()
1194	            .iter()
1195	            .map(|x| x.as_str().unwrap())
1196	            .collect();
1197	        assert_eq!(req, ["adapter"], "only the header is mandatory");
1198	        // The header's own requireds include the compat gate.
1199	        let adapter_req = v["$defs"]["Adapter"]["required"].as_array().unwrap();
1200	        assert!(adapter_req.iter().any(|x| x == "min_spt_core_version"));
1201	        // Closed vocabularies surface as enums (e.g. the update avenues).
1202	        let avenues = serde_json::to_string(&v["$defs"]["UpdateAvenue"]).unwrap();
1203	        assert!(avenues.contains("file_pull") && avenues.contains("delegated"));
1204	        // Doc comments ride along as descriptions (the schema IS documentation).
1205	        assert!(
1206	            s.contains("opaque"),
1207	            "descriptions must carry the contract's language"
1208	        );
1209	    }
1210
1211	    /// The drift gate (M6-D3): the checked-in `manifest.schema.json` — the
1212	    /// copy that ships to the docs site root and the release assets — must
1213	    /// equal what the current derives generate. CI runs this in the normal

        assert!(matches!(e, ManifestError::Parse(_)));
    }

    // ---- M6-D3: the published manifest JSON Schema ----

    #[test] // [unit->REQ-DOCS-4] the generated schema carries the stable $id at
            // the canonical Pages URL (ADR-0014), the adapter header's requireds, and
            // the closed enum vocabularies (the things adapter authors validate against).
    fn manifest_schema_shape() {
        let s = manifest_json_schema();
        let v: serde_json::Value = serde_json::from_str(&s).expect("schema is valid JSON");
        assert_eq!(
            v["$id"],
            "https://sabermage.github.io/spt-releases/manifest.schema.json"
        );
        assert_eq!(v["title"], "spt-core runtime manifest");
        // [adapter] is the only required top-level section.
        let req: Vec<&str> = v["required"]
            .as_array()
            .unwrap()
            .iter()
            .map(|x| x.as_str().unwrap())
            .collect();
        assert_eq!(req, ["adapter"], "only the header is mandatory");
        // The header's own requireds include the compat gate.
        let adapter_req = v["$defs"]["Adapter"]["required"].as_array().unwrap();
        assert!(adapter_req.iter().any(|x| x == "min_spt_core_version"));
        // Closed vocabularies surface as enums (e.g. the update avenues).
        let avenues = serde_json::to_string(&v["$defs"]["UpdateAvenue"]).unwrap();
        assert!(avenues.contains("file_pull") && avenues.contains("delegated"));
        // Doc comments ride along as descriptions (the schema IS documentation).
        assert!(
            s.contains("opaque"),
            "descriptions must carry the contract's language"
        );
    }

    /// The drift gate (M6-D3): the checked-in `manifest.schema.json` — the
    /// copy that ships to the docs site root and the release assets — must
    /// equal what the current derives generate. CI runs this in the normal


Grep

fn parse|from_toml|Manifest::|fn minimal|let m =|adapter_name.*=|name = "


No matches found

[Showing results with pagination = offset: 1000]


fn from_toml|pub fn parse|Manifest::from|toml::from_str.*Manifest|fn .*parse.*Manifest


5://! is the parse+validate front door: [`Manifest::from_toml_str`] deserializes
635:    pub fn from_toml_str(s: &str) -> Result<Manifest, ManifestError> {
839:        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
888:        let err = Manifest::from_toml_str(no_key).unwrap_err().to_string();
897:        let m = Manifest::from_toml_str(ok).expect("file_pull with key parses");
902:        let reparsed = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
908:        let md = Manifest::from_toml_str(deleg).expect("delegated parses");
917:        let m = Manifest::from_toml_str(SHELL_FIXTURE).expect("parse");
929:        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
931:        let reparsed = Manifest::from_toml_str(&serialized).expect("re-parse");
949:        let e = Manifest::from_toml_str(bad).unwrap_err();
964:        let e = Manifest::from_toml_str(bad).unwrap_err();
980:        let e = Manifest::from_toml_str(bad).unwrap_err();
1021:        let parent = Manifest::from_toml_str(
1045:        let e = Manifest::from_toml_str(bad).unwrap_err();
1060:        let e = Manifest::from_toml_str(bad).unwrap_err();
1084:        let m = Manifest::from_toml_str(reuse).expect("reuse-history parses");
1086:        let rt = Manifest::from_toml_str(&m.to_toml_string().unwrap()).unwrap();
1100:        let m = Manifest::from_toml_str(own).expect("own-source parses");
1122:        let e = Manifest::from_toml_str(empty).unwrap_err();
1140:        let e = Manifest::from_toml_str(no_src).unwrap_err();
1156:        let e = Manifest::from_toml_str(zero).unwrap_err();
1162:        let m = Manifest::from_toml_str(
1173:        let e = Manifest::from_toml_str("this is not = = toml").unwrap_err();


833	[shell.sensory]
834	types = ["image", "sound", "event"]
835	"#;
836
837	    #[test] // [unit->REQ-MANIFEST-1]
838	    fn harness_fixture_parses_all_sections() {
839	        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
840	        assert_eq!(m.adapter.name, "claude-spt");
841	        assert_eq!(m.adapter.kind, AdapterKind::Harness);
842	        assert_eq!(m.adapter.hostable_types.len(), 4);
843	        // hooks
844	        assert!(m.hooks["SessionStart"].can_inject);
845	        assert!(!m.hooks["Stop"].can_inject);
846	        assert_eq!(
847	            m.hooks["SessionStart"].reads,
848	            ["session_id", "source", "agent_type"]
849	        );
850	        // session: dirs + one role
851	        assert_eq!(m.session.commune_dir.as_deref(), Some(".claude"));
852	        let role = m.session.psyche_init.as_ref().unwrap();

[shell.sensory]
types = ["image", "sound", "event"]
"#;

    #[test] // [unit->REQ-MANIFEST-1]
    fn harness_fixture_parses_all_sections() {
        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
        assert_eq!(m.adapter.name, "claude-spt");
        assert_eq!(m.adapter.kind, AdapterKind::Harness);
        assert_eq!(m.adapter.hostable_types.len(), 4);
        // hooks
        assert!(m.hooks["SessionStart"].can_inject);
        assert!(!m.hooks["Stop"].can_inject);
        assert_eq!(
            m.hooks["SessionStart"].reads,
            ["session_id", "source", "agent_type"]
        );
        // session: dirs + one role
        assert_eq!(m.session.commune_dir.as_deref(), Some(".claude"));
        let role = m.session.psyche_init.as_ref().unwrap();


Add the manifest unit test (declared vs absent + N-1 round-trip):


Edit

// ---- M6-D3: the published manifest JSON Schema ----

    // [unit->REQ-MANIFEST-7] the optional adapter-declared shortcut basename:
    // absent ⇒ None (picker falls back to "spt" → spt-<id>); declared ⇒ read
    // verbatim (an adapter brands its shortcut, e.g. cc → cc-<id>); omitted from
    // serialization when absent (additive + N-1-safe).
    #[test]
    fn shortcut_basename_optional_with_default() {
        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
        assert_eq!(m.adapter.shortcut_basename, None, "absent ⇒ None");

        let branded = HARNESS_FIXTURE.replace("[adapter]", "[adapter]\nshortcut_basename = \"cc\"");
        let m2 = Manifest::from_toml_str(&branded).expect("parse branded");
        assert_eq!(m2.adapter.shortcut_basename.as_deref(), Some("cc"));

        // N-1-safe serialization: the field is omitted when absent, present when set.
        assert!(!m.to_toml_string().unwrap().contains("shortcut_basename"));
        assert!(m2.to_toml_string().unwrap().contains("shortcut_basename"));
    }

    // ---- M6-D3: the published manifest JSON Schema ----


    // [unit->REQ-MANIFEST-7] the optional adapter-declared shortcut basename:
    // absent ⇒ None (picker falls back to "spt" → spt-<id>); declared ⇒ read
    // verbatim (an adapter brands its shortcut, e.g. cc → cc-<id>); omitted from
    // serialization when absent (additive + N-1-safe).
    #[test]
    fn shortcut_basename_optional_with_default() {
        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");
        assert_eq!(m.adapter.shortcut_basename, None, "absent ⇒ None");

        let branded = HARNESS_FIXTURE.replace("[adapter]", "[adapter]\nshortcut_basename = \"cc\"");
        let m2 = Manifest::from_toml_str(&branded).expect("parse branded");
        assert_eq!(m2.adapter.shortcut_basename.as_deref(), Some("cc"));

        // N-1-safe serialization: the field is omitted when absent, present when set.
        assert!(!m.to_toml_string().unwrap().contains("shortcut_basename"));
        assert!(m2.to_toml_string().unwrap().contains("shortcut_basename"));
    }

    // ---- M6-D3: the published manifest JSON Schema ----

+    // [unit->REQ-MANIFEST-7] the optional adapter-declared shortcut basename:

+    // absent ⇒ None (picker falls back to "spt" → spt-<id>); declared ⇒ read

+    // verbatim (an adapter brands its shortcut, e.g. cc → cc-<id>); omitted from

+    // serialization when absent (additive + N-1-safe).

+    fn shortcut_basename_optional_with_default() {

+        let m = Manifest::from_toml_str(HARNESS_FIXTURE).expect("parse");

+        assert_eq!(m.adapter.shortcut_basename, None, "absent ⇒ None");

+        let branded = HARNESS_FIXTURE.replace("[adapter]", "[adapter]\nshortcut_basename = \"cc\"");

+        let m2 = Manifest::from_toml_str(&branded).expect("parse branded");

+        assert_eq!(m2.adapter.shortcut_basename.as_deref(), Some("cc"));

+        // N-1-safe serialization: the field is omitted when absent, present when set.

+        assert!(!m.to_toml_string().unwrap().contains("shortcut_basename"));

+        assert!(m2.to_toml_string().unwrap().contains("shortcut_basename"));

#[test] // [unit->REQ-DOCS-4] the generated schema carries the stable $id at


data.rs helper to resolve basename from the adapter manifest + wire mod.rs:


/// Every discoverable endpoint, across all three categories. The model

/// 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