# v0.14.0 — Endpoint creation flow (multi-subnet home + Unbound state): JIT build plan

**Design (operator grill, 2026-06-22):** ADR-0026 (multi-subnet home at `endpoint run` creation) + ADR-0027 (Unbound endpoint state + attach-on-session-exists). CONTEXT.md term: **Unbound endpoint**. Two REQs registered (doc-active, this branch): `REQ-RUN-MULTISUBNET-HOME`, `REQ-ENDPOINT-UNBOUND-ATTACH`. MINOR bump (new endpoint-creation UX + a new on-disk liveness status).

**PREREQUISITE (binding):** start v0.14.0 from `main` **after v0.13.2 has merged** — this milestone builds on v0.13.2's broker `sessions` map carrying the per-endpoint `endpoint`/`adapter`/`install_dir` fields (ADR-0025 W3a), the source of truth for "is there a live session for this id." Rebase/merge this design branch (ADR-0026/0027 + the two REQs + this JIT) onto post-v0.13.2 main before activating.

**Motivating real-world case:** perri's multi-subnet bringup gap (operator-hit, 2026-06-22) — a **latent** gap (not a regression; `HOME_REFUSED` is the established ≥0.11.0 no-guess policy), exposed when the node crossed 1→2 subnets (BIGNET added). The spt-hosted bringup never supported a multi-subnet home. W2 closes it; perri's repro is the W6 regression test (**MUST run on a multi-subnet node** — a single-subnet node auto-homes and hides the gap).

**The two "resident" facts that shape the waves** (verified at code, the grill):
- Home is **immutable** (ADR-0010). `establish_perch → stamp_creation_fields` (home.rs:122) **inherits** a prior perch's `home_subnet` (prior-branch) and only `assign_home`s on the fresh (None) branch. So the gap is **only first-creation** on multi-subnet; pre-creating a skeleton perch WITH the home makes bind inherit it — no hook/env change.
- The broker (`spt daemon run`) and the brain (`spt daemon brain`) are **separate processes** (v0.13.2 W3c finding); the broker `sessions` map is the IPC-reachable, brain-restart-surviving source of "live session." Attach + the Unbound display both read it.

---

## W1 — `STATUS_UNBOUND` status primitive + lifecycle  (REQ-ENDPOINT-UNBOUND-ATTACH, ADR-0027)
Foundational — the skeleton (W2), the attach (W4), and the display (W5) all key on it.
- **Scope:** add `STATUS_UNBOUND` (`liveness.rs:37`, beside `STATUS_ONLINE`/`STATUS_OFFLINE`). Lifecycle transitions: spawn / skeleton-create → `unbound`; `bind` (cmd_bind establish-online) → `online`; broker session death → `offline` via the EXISTING exit-waiter/reconcile (the B2/B5 dead-but-latched path — reuse, no new teardown). Liveness predicates: an `unbound` perch is **attachable** (a live broker session exists) but **NOT message-addressable** (no bound `session_id`) — `is_perch_alive` / deliver resolution stay gated on `online`/bound; a new "has a live session" predicate (reads the broker sessions map) backs attach + display.
- **Files:** `crates/spt-store/src/liveness.rs` (the constant + predicates); the reconcile/exit-waiter that stamps offline (livehost.rs / broker session-exit path); `info.json` status writer.
- **Gate:** unit (the transitions; `unbound` is attachable-not-addressable; reconcile resolves a dead-session `unbound` → `offline`). doc = ADR-0027 + CONTEXT (active). No int yet (composed in W6).

## W2 — Home resolution + skeleton-with-home  (REQ-RUN-MULTISUBNET-HOME, ADR-0026) — the multi-subnet UNBLOCK
- **Scope:** at `endpoint run`'s skeleton-create step, resolve the home and **pre-create the skeleton perch** (status `unbound` from W1, `home_subnet` set) BEFORE spawning the harness; the hook's `bind` then inherits home via `establish_perch`'s prior-branch (UNBOUND→ONLINE). Resolution policy:
  - sole subnet → auto (`assign_home`, unchanged);
  - multi-subnet + no `--subnet` + **non-interactive** (`std::io::IsTerminal` on stdin, the cli.rs:1006/3956 precedent) → **refuse early** (`MULTI_SUBNET_HOME:` clear message + the subnet list) — never the silent 25s timeout;
  - multi-subnet + no `--subnet` + **interactive** → print the proposed config (id / project / adapter[:profile] / home = the default) + `Ok to proceed? Y/n`; `n` → print the `--subnet` guidance;
  - `--subnet <name>` overrides + validates membership (`assign_home` `NotMember`).
  - The default home (for the confirm) = a **node-global last-used** value to start (W3 upgrades it to the two-level lists).
- **Files:** `cmd_endpoint_run` (cli.rs:~1384, where it spawns straight to `launch_harness_brokered_in` today — add the resolve + skeleton-write before it); a `--subnet` arg on `endpoint run`; `assign_home` (home.rs:71) reused; the skeleton write via `info::write_info` (status `unbound`, `home_subnet`, `state=live_agent`, `adapter`, `cwd`). Conflict-safe: a skeleton has an empty `session_id`, so `establish_perch`'s Conflict check (owner-alive ∧ session-mismatch) never trips.
- **Gate:** unit (resolution matrix: sole-auto / multi-noninteractive-refuse / multi-interactive-confirm / --subnet override+NotMember; the skeleton carries the resolved home; bind inherits it via prior-branch). doc = ADR-0026 + CONTEXT. (int in W6.)

## W3 — Two-level MRU home lists  (REQ-RUN-MULTISUBNET-HOME)
Upgrades W2's single-value default to the full UX.
- **Scope:** persisted **ordered move-to-front LISTs** of recently-chosen home subnets at **two levels**: per-project (keyed by project_id) + an always-updated node-global list (the fallback for a project with no list yet). The interactive default home = project-list head ?? node-global head; the interactive confirm + the `--subnet` refusal guidance list subnets **MRU-ordered**. Both lists update (move-to-front) on every home choice (run-confirm or explicit `--subnet`).
- **Files:** a small MRU store under `$SPT_HOME` (node-global) + per-project (project store / `p-<project>` adjacency or a sibling file); the `endpoint run` resolve path reads/writes it; project_id from the run cwd (`project_id_for_dir`).
- **Gate:** unit (move-to-front; project-then-node-global precedence; node-global fallback for an MRU-less project; the guidance/confirm ordering). doc = CONTEXT (the MRU note on the home-subnet term).

## W4 — Attach-on-session-exists  (REQ-ENDPOINT-UNBOUND-ATTACH, ADR-0027)
- **Scope:** gate the attach on the broker **session being attachable**, not on perch `STATUS_ONLINE`. Replace `await_endpoint_online` (cli.rs:1434) with an await-session-attachable poll (reads the broker sessions map / a "has live session" query, the W1 predicate). `cmd_endpoint_run` AND `spt rc <id>` attach to a live session **regardless of perch status** (so an `unbound` endpoint is attachable — headless bringups too, and the operator can clear a bind-gating prompt). Preserve REQ-HAZARD-RC-ATTACH-ONLINE-RACE's "no attach before a session" intent at the earlier session-exists point; bounded deadline; local-only (a remote row can't see another node's broker).
- **Files:** `cmd_endpoint_run` (cli.rs:1426-1447, the await+attach block); `run_attach` / `spt rc` status-gate (rc.rs) — attach when a session exists, not only when online; the broker "has live session for id" query (over the W3a sessions map).
- **Gate:** unit (the gate keys on session-exists not perch-online; the offline-but-session-live case attaches; no-session still awaits/fails). int folded into W6.

## W5 — `Unbound` display  (REQ-ENDPOINT-UNBOUND-ATTACH)
- **Scope:** `EpDisplay` gains `Unbound` (picker/model.rs:55; today Offline/Online/HarnessOnly/Controlled). Render **hollow** (the existing filled/hollow-square precedent) + a **hollow-controlled** variant (an `unbound` endpoint a node is driving) — paralleling filled Online vs Controlled. **NOT amber** — amber is `HarnessOnly` (online-but-not-broker-controllable), the semantic opposite of `Unbound` (which IS broker-attachable). Derivation: `STATUS_UNBOUND` (+ `driven_by` for the controlled variant). Surface in the picker, `endpoint list`, and `whoami`. Remote rows render plain (the existing controllability limitation, picker/data.rs:254).
- **Files:** picker/model.rs (`EpDisplay::Unbound` + label/legend + the type-gated derivation), picker/data.rs (derive from status), `endpoint list`/`whoami` renderers.
- **Gate:** unit (status `unbound` → `EpDisplay::Unbound`; + driven_by → the controlled variant; offline/online/harness-only unchanged; remote renders plain). doc = the picker legend.

## W6 — int keystone + the multi-subnet regression  (both REQs)
- **Scope:** real-daemon e2e (reuse the v0.12.1 dummy-harness fixture, NO mocks):
  - **perri's multi-subnet repro (the regression test, MUST run on a multi-subnet node):** join a 2nd subnet → `spt endpoint run --adapter X --id Y` (no `--subnet`, non-interactive) → REFUSES clearly (not a 25s timeout); with `--subnet S` → the skeleton homes to S, the harness binds (UNBOUND→ONLINE), no `HOME_REFUSED`. A single-subnet control proves auto-home still works.
  - **attach-before-bind:** `endpoint run` (or `spt rc`) attaches while the endpoint is `unbound` (session live, not yet bound), the operator drives it, then bind flips it ONLINE.
  - **interactive confirm:** (where a TTY can be simulated) the proposed-config + Y/n path; non-interactive refuse.
- **Activate:** `REQ-RUN-MULTISUBNET-HOME` → `[doc,impl,unit,int]`; `REQ-ENDPOINT-UNBOUND-ATTACH` → `[doc,impl,unit,int]`. Flip ADR-0026/0027 status → **accepted** (with any build amendments, the ADR-0025 precedent).
- **Gate:** doyle gates the int hard against perri's repro; **perri validates on real claude-spt at publish** (her queued v0.13.3/v0.14.0 validation).

---

## Build order, gating, ship
**Order:** W1 (status primitive) → W2 (home resolve + skeleton — the unblock) → W3 (MRU lists) → W4 (attach-on-session) → W5 (Unbound display) → W6 (int + regression). W2/W4/W5 share the broker sessions map + the perch status seam — sequential, doyle gates each (CI green BOTH runners per wave; the contended Windows host can't run the lifecycle/daemon suite — CI is authority). Activate each REQ's real stages at its wave (rule 5).

**Scope note (operator decision):** shipped as ONE milestone (not split into a minimal-unblock fast-follow) — the operator accepted staying multi-subnet-blocked until the cohesive landing. The whole thing is v0.14.0.

**Downstream:** perri drops her temp `--subnet` hook stopgap (she declined it — never applied; she's been multi-subnet-blocked by choice). She validates her exact repro on real claude-spt at publish. Coordinate the v0.14.0 publish ping to perri.

**Numbering note:** ADR-0024/0025 ship with v0.13.2; ADR-0026/0027 are this milestone (stable).
