# W0 span verb + picker + docs — JIT build plan (todlando, 2026-09-26)

State when written: legs 1-3, 5, 6 BUILT in `.worktrees/351-w0` (uid, sibling join,
backfill mint at establish_perch, held_elsewhere refusal at bind + skeleton writer,
ENDPOINT_SPAN surface row, sibling bypass at gate.rs step 1b + units). Lane notes:
`w0-lane-notes.md`. Remaining: span op (local + wire), picker, CONTEXT/docs, int.

## A. spanop.rs (spt-daemon, modeled on forkop.rs) — ONE implementation
`pub enum SpanLocalOutcome { Spanned{adapter}, NoMind, AlreadyHere, NoEndpoint, NoAdapter(String), BadRequest(String), Failed(String) }`
`.token()` -> spanmsg::token::{SPANNED,NO_MIND,ALREADY_HERE,NO_ENDPOINT,NO_ADAPTER,BAD_REQUEST,FAILED}.

Pure planner `plan_span(id, adapter_override, perch_exists, mind_uid, regs, own_node) -> Result<SpanPlan, SpanLocalOutcome>`:
1. validate id; `engineroom::reserved_id_refusal` -> BadRequest; PerchKind must be Self_.
2. perch_exists -> AlreadyHere.
3. mind_uid None -> NoMind (ContextStore::endpoint_uid, the READ form — never ensure).
4. sibling rows = other-node rows for id whose endpoint_uid == Some(mind uid); none -> NoEndpoint
   (not a create verb; also refuses when the only rows are a different/absent uid).
5. home subnet = first (BTreeMap order) snapshot subnet holding a sibling row.
6. adapter = override, else first sibling row `adapter`; none -> NoAdapter.
7. rest: dormant if any sibling row is Status::Active.
`span_local(id, adapter_override, now_unix)`: plan with real inputs
(`presence::load_registry_snapshots(identity_dir/registry)`, `presence::local_node_hex`),
resolve adapter as registered harness (`spt_runtime::registry::resolve_option`,
kind Harness) else NoAdapter; write skeleton: `InfoJson::new(id, now, pid, "", "live_agent")`,
adapter, home_subnet, endpoint_uid=Some(uid), rest_state dormant+dormant_since_ms when a
sibling is active; `create_dir_all` + `write_info` + `set_status(UNBOUND)`;
`home::seed_endpoint_scopes(vis, id, home)`; `registryhost::request_advertise_now()`.

## B. wire (mirror fork exactly)
- `spt-net/src/net/spanmsg.rs`: SPAN_KIND="span", `SpanRecord{kind, op_id, endpoint, adapter: Option, requester_node_claimed}`, `SpanReply{outcome, detail}`, decoders, `token` mod; register in `net.rs`. Units: round-trip carries tag; field set pinned.
- dispatch.rs: `StreamFamily::Span` (+ALL[19], classify arm `Some(k) if k == SPAN_KIND`, serve arm, `serve_span_feed` = copy of serve_fork_feed, `pub fn serve_one_span(rec, origin)`: `access_check(&rec.endpoint, origin, surface::ENDPOINT_SPAN, Unsolicited).is_deny()` -> None; else span_local; on Spanned launch the session via `harnesshost::launch_harness_brokered_in(broker_name, id, adapter, mint_session_id(), &manifest, None, false, Some(&record.source_dir), node_label)` (autostart.rs pattern; broker name = how daemon.rs passes it to replay_startup_endpoints). Fix the two wildcard-free test matches (`=> 18`, the `|` list) + census test.
- wan.rs: `SPAN_VERB`, `SpanRequestOutcome{Spanned, Refused{token,detail}, NoReply, PeerSilent}`, `request_span` (copy request_fork); export in lib.rs `pub use wan::{...}`.
- wansend.rs: `span_send_remote(id, node_hex, adapter) -> SpanSendOutcome`, `classify_span_reply` pure (NoReply => Unconfirmed = SPAN_UNCONFIRMED; never success). Dial: `seed_first_addr(node_hex)`.

## C. CLI `spt endpoint span <id> [--node N] [--adapter A]`
EndpointCmd::Span in cli.rs (next to Start; also the Door/Bringup table ~3491 if exhaustive).
`--node` via `resolve_node_spelling` (self/label/prefix -> hex). node absent or == own -> local arm:
`access_check(id, own_hex, ENDPOINT_SPAN, Unsolicited)` (LocalNode origin -> only a rule blocks),
`span_local`, then `cmd_endpoint_start(id, Some(adapter), None)`. Else remote arm.
Output tokens: `SPANNED:<id> node=<n>`, `SPAN_NO_MIND` / `SPAN_ALREADY_HERE` / `SPAN_NO_ENDPOINT` /
`SPAN_NO_ADAPTER` / `SPAN_REFUSED` / `SPAN_UNCONFIRMED` / `SPAN_PEER_SILENT`, each naming what to do.
Units: plan_span matrix (every refusal + happy + dormant landing), classify_span_reply, local arm
under isolated_home writes skeleton with uid and starts nothing when refused.

## D. picker (REQ-PICKER-SPAN-TO-NODE)
model.rs ~1360: replace remote-row `Instantiate` with `ConfirmOption::Span`; ALSO offer Span on a
local row. New `Screen::SpanPick` (ChooseProject pattern): nodes = this node (only if no instance
here) + roster members of the subnets where the id is advertised, minus nodes hosting an instance.
Label by node label. Choosing runs the same verb (`spt endpoint span <id> --node <hex>`). view.rs label
"Span to node". Update tests at model.rs ~2452-2520 (Instantiate expectations).

## E. docs
CONTEXT.md: define endpoint uid (identifies, never authenticates, not a secret), span, sibling
bypass, R4-6 trust boundary (holding the mind = may become a sibling); amend the
instantiate-anywhere line (same-identity new-node instance = span under ENDPOINT_SPAN; fresh spawn
by another node's AGENT stays deferred). `<!-- [doc->REQ-...] -->` for UID, SIBLING-JOIN,
SPAN-VERB, SPAN-SURFACE, SIBLING-BYPASS. docs-site/src/instances/overview.md: `spt endpoint span`
section. Regenerate CLI reference (xtask docs) if the CLI tree changed.

## F. int (two-host) — crates/spt-daemon/tests/twohost_span.rs (+ W1 twohost_axes.rs copied in)
One-box dry run (127.0.0.1, separate SPT_HOMEs) then kitsubito (git bundle; PATH=$HOME/.cargo/bin).
(1) span B from A, ENDPOINT_SPAN open -> both rows coexist, no REGISTRY_COLLISION over >=3 rounds;
(2) target closed, no rule -> refused (SPAN_UNCONFIRMED: refusal sends nothing), nothing spawned;
(3) different-uid same-name endpoint still collides.

## Before the gate
Rebase feat/351-w0-span onto W1 835821aa. treqs from `git archive <sha>` (NOT the worktree:
untracked twohost files carry int tags). Commit refs (releases#354), trailer last.
