# W0 #354 SIBLING COEXISTENCE — census + picks (todlando → doyle, 2026-09-26)

Branch `feat/351-w0-span` off 6680af98, worktree `.worktrees/351-w0`, pool claimed. No impl. Raw census with file:line: `.spt/preserved/354/w0-census-notes.md`.

## Census facts that shape the picks
- **The mind is created LAZILY.** `a-<id>` first appears on the first commune, role write, monic, or sync adopt (`ingest.rs:201`, `cli.rs:5693`, `syncmerge.rs:131`). No creation site writes it eagerly.
- **Fork copies EVERY file** on `a-<id>` except `.conflicts/` (`contextstore.rs:411-453`). One implementation (`forkop::fork_local`, `forkop.rs:119`) serves both local and wire fork. **Rename** keeps the branch (`git branch -m`), so the uid carries. **Purge** deletes the branch, on this node only.
- **Sync reaches every roster node in scope.** `pump/sync.rs:96-104` bootstrap-pulls every `a-<id>` named in the shared subnet's registry, when `SyncPolicy::allows` holds (roster member AND `vis.synced`). This matches R4-6's accepted boundary.
- **Merge is per file, by version vector.** Two markerless mints of one file are CONCURRENT and produce a surfaced conflict artifact; the local copy is untouched (`syncmerge.rs:103-113`). The LLM reconcile excludes only `live-role.md` (`sync.rs:552-562`).
- **Collision checks:**
  - At advertise: `advertise_local` → `advertise_if_visible` → `join_endpoint` (`registryhost.rs:752-771`) is the ONLY production caller. Wire rows go straight to `merge_instance`.
  - At fork mint: registry holders (`forkop.rs:95-110`), local perch taken, and `a-<new>` already exists.
  - `cmd_rename` checks only `has_branch(a-<new>)`. The registry's `rename_endpoint` has NO production caller.
- **Holder refusal is missing.** No local bring-up path refuses an id held on another node: `create`, `run`, `api listen/bind`, and the picker's Instantiate all mint a colliding perch. `start`, `resume` and `go` refuse only as "unknown". There is no `spt live` verb in core; `/sptc:live` reaches `api listen` → `establish_perch`.
- **Access chain.** There is ONE body (`gate.rs:220` `access_check_with_sender`), and the engine-room lock runs as step 0 (`:264`) for every caller, ahead of the reply exemption, degrade and `decide` steps.
  - 12 production call sites. A sender id is proven at only 2 of them: `receive_wan` (MSG) and `answer_presence_probe`.
  - FORK, REST, ATTACH, SHELL_LINK, DIGEST and WEB know only the origin node.
  - The Subject enum is origin-side only (SenderEndpoint / Node / SubnetWildcard). SenderEndpoint is "dead on arrival" on a non-attributable surface.
- **Surfaces** are a single-source `TABLE` (`access.rs:207`), so a new surface is a one-row edit. FORK's row: `attributable: false, default_on: false`.
- **Brain constraint.** `rollback_compat.rs:7-11`: a brain must not irreversibly migrate durable state before ready-promotion.

## Picks (one per census question)

**(1) Where the uid lives.**
- A file `endpoint-uid` on `a-<id>`: 32 lowercase hex characters (128 random bits), written through `commit_live`.
- Cached in `info.json` as `endpoint_uid: Option<String>`, stamped when minted or adopted. `advertise_local` reads the cache, so there is no git read on every round.
- Carried as `Instance.endpoint_uid: Option<String>` (`#[serde(default, skip_serializing_if = "Option::is_none")]`), so an N-1 row reads None.
- Added to the LLM-reconcile exclusion beside `live-role.md`.

**(2) The join rule.**
- Holders = other-node rows EXCEPT those whose uid is `Some` and equal to the incoming `Some` uid. A uid absent on either side COLLIDES: a no-uid peer still clashes as it does today, so an upgrade window cannot mint false siblings.
- Fork's mint check keeps "any holder of `new_id` = Taken". The new id is a different endpoint, so uid equality does not apply.
- **Fork mints a NEW uid:** exclude `endpoint-uid` in fork's `copy_mind_files` keep-closure, then mint fresh for `new_id` in the same `commit_live`.
- Rename carries the uid, which is correct. The dead registry `rename_endpoint` is left alone and flagged.

**(3) First-boot backfill.**
- Mint where the perch is HOSTED and the mind has no uid: in `establish_perch` (both the new-perch arm and the revive arm) and in the `endpoint run` skeleton.
- `advertise_local` ADOPTS an existing mind uid into a missing cache (a `read_at_tip` once per perch, the `adopt_for_unset` pattern). It never mints.
- A node that holds only the replicated mind (no perch) NEVER mints.
- Safe today: before W0, at most one node per subnet could advertise an id, so there is one minting host per endpoint.
- Residual: two pre-existing same-name endpoints (a real clash) mint two uids. Sync surfaces a conflict on `endpoint-uid` and the registry keeps colliding, which is the correct outcome for a genuine clash.
- Nothing mints in the brain before ready-promotion: bind and run are CLI-side.

**(4) The span verb.** `spt endpoint span <id> [--node N] [--adapter A]`.
- **Local arm** refuses unless:
  - `has_branch(a-<id>)` holds AND the tip has an `endpoint-uid` (`SPAN_NO_MIND`);
  - there is no local perch yet (`SPAN_ALREADY_HERE`);
  - an instance of the id is visible in the registry (`SPAN_NO_ENDPOINT`: this is not a create verb).
- It then writes a perch skeleton with the cached uid and the rest state from the boot rule (dormant if a sibling is active). The adapter comes from a sibling row's gossiped `Instance.adapter` unless `--adapter` is given; if neither, refuse with `SPAN_NO_ADAPTER`. Then it brings the session up exactly like `endpoint run`.
- **Remote arm** is a new wire family `span` modeled on fork:
  - `SpanRecord{kind, op_id, endpoint, requester_node_claimed}` / `SpanReply{outcome, detail}`;
  - tokens `spanned` / `no_mind` / `already_here` / `no_endpoint` / `no_adapter` / `failed`;
  - no reply is never success (`SPAN_UNCONFIRMED`).
- **Holder refusal** for R4-1's "`spt live <id>` refuses and names span": one `held_elsewhere(id)` check (registry rows for the id on another node, no local perch). It sits at the three perch-minting sites: `establish_perch`'s new-perch arm (new `BindError::HeldElsewhere`), `resolve_home_and_write_skeleton` (create/run), and the picker Instantiate. Each refusal says ``spt endpoint span <id>``.

**(5) `ENDPOINT_SPAN`.**
- One `TABLE` row: `{ id: "ENDPOINT_SPAN", attributable: false, default_on: false, desc: "spanning this endpoint to this node (a sibling instance)" }`.
- Evaluated on the TARGET: `access_check(endpoint, origin, surface::ENDPOINT_SPAN, Unsolicited)`. The endpoint id is the rule target (per-endpoint rules are string-keyed, so they apply before any perch exists), with node rules/modes after.
- **R4-3's subjects map onto the existing Subject kinds:**
  - "its node" = `Subject::Node` (all of that node's endpoints);
  - "its subnet" = `Subject::SubnetWildcard`;
  - "the spanned endpoint" = a per-ENDPOINT rule on the target node (rule target = the id), NOT a SenderEndpoint subject. The request carries no proven sender, and fork sets the same precedent.
  - **Ask:** confirm that reading of "subject = the spanned endpoint". The alternative is attributable plus a daemon-stamped sender, which the wire does not carry today.
- **R4-4 (span run on the target itself):** the local origin reads `Origin::LocalNode`, so the chain bottom is `Allow(SameNode)`. Only an explicit node-local deny rule blocks it, which is RULES-ONLY exactly as ruled. No new code.

**(6) Sibling bypass — ONE site.**
- It goes INSIDE `access_check_with_sender`, AFTER step 0 (the engine-room lock still precedes it; I confirm your recommendation) and BEFORE `decide`.
- It allows when all of these hold:
  - the origin is remote;
  - `bare(sender_proven) == endpoint`;
  - the origin node has a registry row for `endpoint` whose `endpoint_uid` is `Some` and equals the local perch's `endpoint_uid`.
- It returns `AccessDecision::Allow(Sibling)`, a new variant, so the chain's tier report names it.
- **Reach:** only the sites that carry a proven sender, which are `receive_wan` MSG and the presence probe. REST, ATTACH, SHELL_LINK, DIGEST, FORK and WEB carry no sender and stay under the rules. **Ask:** is sibling WAKE/SUSPEND (the B-lane handoff) in scope for W0? If yes, stamp `sender_proven` on `RestRecord`; otherwise it stays a B-lane item.
- Registry demotion and mind sync sit OUTSIDE the chain, gated by roster membership; they need no bypass.

**(7) Picker "Span to node".**
- Replace the remote-row `Instantiate` slot (`model.rs:1360-1363`) with `ConfirmOption::Span`. It opens a new `Screen::SpanPick`, following the `ChooseProject` pattern.
- The list = this node plus the roster members of the subnets where the id is advertised, minus nodes already hosting an instance. It is labelled by node label.
- Choosing an entry calls the same `span` verb (local or `--node`). The target answers `no_mind` when it lacks the mind; the picker does not pre-judge.
- A local row gets no Span option (it is already here). Span to OTHER nodes is also possible from a local row, so **Ask:** remote rows only, or both?

## New REQs I'd mint (`required_stages = []` until you rule)
- REQ-ENDPOINT-UID (mint, mind storage, cache, row carry, fork re-mint, backfill)
- REQ-SIBLING-JOIN (uid-equal join, absent/different collides)
- REQ-ENDPOINT-SPAN-VERB (local + remote arms, refusals, held-elsewhere refusal at mint sites)
- REQ-ENDPOINT-SPAN-SURFACE (the `ENDPOINT_SPAN` row, target-evaluated)
- REQ-SIBLING-BYPASS (the one chain site, after step 0)
- REQ-PICKER-SPAN-TO-NODE
- CONTEXT.md: the R4-6 trust-boundary statement

Stages: doc/impl/unit on all; int on SIBLING-JOIN + SPAN-VERB (two-host), which also unblocks W1's int.
