# NEXT MILESTONE — REMOTE-TRUTH (v0.24.0 field batch): triage + work order

Status: **TRIAGED 2026-07-04, all items code-anchored, design rulings grilled
+ committed.** Author: doyle. Inputs: operator field batch (10 items, 2026-07-04),
five recon sweeps (anchors verified @ed1eb08 era), grill session Q1–Q5
(operator-ruled; glossary commits @130ca6c, @c248afc; ADR-0033/0034).
Runs beside the F-030 PSYCHE-EPHEMERAL wave (separate branch/chunks); the
relay-unregister casualty rider lands here, NOT in F-030.

Grill rulings this doc bakes in:
- **Q1 (@130ca6c):** effective instance state is DERIVED — liveness
  discriminates warm/cold; rest intent refines within warm; absent intent
  never defaults active. One shared derivation for every reader.
- **Q2 (ADR-0033):** wake's resume is RECONCILE-DRIVEN — thin wake edge writes
  intent; the daemon resume leg does the lift; non-blocking default,
  `--wait` flag rider.
- **Q3:** bare-id rest verbs route subnet-wide (send's fallback); one
  instance → act; many, none active → refuse loud with candidates; one
  active → no-op naming the node.
- **Q4 (ADR-0034):** per-minter journal-key namespacing + auto-retry-once on
  tracing-only op families; typed op errors become internal-only; the
  public error surface never speaks core lingo.
- **Q5 (@c248afc):** a session's adapter is recorded truth; resume follows
  the session; unregistered recorded adapter refuses loud; absent record
  degrades benign.

---

## A — Wake / rest-state cluster (operator #5; the wave heart)

Four shapes, four roots, one discipline (recon: full lifecycle map, all
writers/readers anchored).

**A-1 — void-intent misread (`NO_EDGE` on a definitely-suspended endpoint).**
Root (certain): `apply_event` derives `from` off the stored field alone and
defaults void → Active (resting.rs:224); a cold perch with no intent answers
"already in target state". This IS the banked F-028 "rest_state void" seed.
Fix: the Q1 shared derivation — one function (liveness + unbound subtlety +
intent) used by `advertised_status` (registryhost.rs:821) AND `apply_event`;
void + cold ⇒ Suspended. Red-first: perch `status="offline"`, no rest_state →
`daemon_rest_event(Wake)` yields `Suspended -> Active`, not `Ok(None)`.

**A-2 — resumed-on-wake unwired (`WOKE:… -> Active` yet still suspended).**
Root (certain): wake edge writes only `rest_state` (resting.rs:439); roster's
cold arm ignores it (registryhost.rs:831); livehost reconcile start-side only
maintains `status=="online"` endpoints (livehost.rs:471). CONTEXT:598 promises
resume-on-wake; nothing implements it. Fix (ADR-0033): daemon reconcile gains
a **resume leg** — intent active + session cold ⇒ resume via the adapter's
`[session.resume]` with the last ledger sid, mirroring shellwake's
relaunch-offline-shells (shellhost.rs:520); `status=online` still set ONLY by
real bind. `spt endpoint wake` default prints accepted-not-bound truth;
`--wait` blocks on bind (reuses F-027 bind-await machinery when that lands).
Composes with item D-2: the resume leg reads the ledger row's recorded
adapter. Red-first int: wake a suspended live agent → status reaches online,
reconcile re-hosts (today: neither).

**A-3 — bare-id wake fails local-only (`not a hosted perch`).**
Root (certain): `cmd_rest` routes remote only on `@`/`:` (cli.rs:3296); no
local-miss fallback, unlike `cmd_send` (cli.rs:5142 → resolve_across_visible).
Contradicts CONTEXT:286 ("a wake must route"). Fix (Q3): adopt send's
fallback for rest verbs + the Q3 selection rule (one → act; many-none-active →
`WAKE_AMBIGUOUS` listing candidates; one-active → no-op naming node).
Red-first: bare id present only in a remote registry snapshot routes instead
of `WOKE_FAIL`.

**A-2 ruling addendum (doyle, 2026-07-05 — build recon: the "loud, never
silent" refusal had no surface):**
- **New additive `info.json` field `host_error: Option<String>`** — the most
  recent host-level failure that prevented the daemon from hosting/resuming
  this endpoint (serde-default additive, exact `psyche_host_error` pattern —
  which stays psyche-scoped; overloading it was rejected as wrong semantics).
  Written by the resume leg on an unregistered-recorded-adapter refusal (F-1
  language, names the adapter + the `spt adapter add` next action); ALSO
  stamped by the existing reconcile start-arm's deregistered-adapter path
  (livehost.rs:205 `continue`s silently today — same silent-failure class,
  same seam, folded in). CLEARED on a successful host/bind. NEVER read by
  liveness or the A-1 effective-state derivation — it is a report, not a
  state input.
- **Surfaces**: `spt endpoint list` renders it beside liveness (joins the
  F-1 sweep's existing render seed with `translation_fault` +
  `psyche_host_error` — three fields, one render pass); AND `spt endpoint
  wake`'s accepted-not-bound print includes the current `host_error` when
  present (the user who just asked is the one who needs it).
- **Double-launch guard**: resume-pid marker in the live perch mirroring
  `resolve_wake`'s live-pid probe — pid alive ⇒ stand down (boot window),
  pid dead pre-bind ⇒ retry, bind closes the gate. The marker is
  CUSTODY-ONLY: never a liveness input (A-1 derivation must not read it).
- **`--wait` deferred** to its own rider seed (F-027 bind-await is
  design-only today; the core resume leg does not block on it).
- Reconcile-once start-side branch confirmed as the home (the seam
  livehost.rs:176-184 already carves by name); cold-with-no-ledger-row =
  benign loud-logged skip. Psyche boundary confirmed CLEAR: the leg is a
  harness spawn that self-binds; it only READS the last ledger sid; zero
  W3-retired machinery touched.

**A-3 ruling addendum (doyle, 2026-07-05 — build recon surfaced a substrate
gap + two Q3 edges the operator ruling didn't state):**
- **Substrate:** `resolve_across_visible` filters routable-only and its
  ambiguity payload carries no per-candidate status — Q3 is NOT expressible
  through it. A-3 gets its own pure `select_rest_target(candidates:
  (node,status), verb)` selector built on `reg.instances(id)`;
  `resolve_across_visible` and `cmd_send` stay untouched. Candidate status is
  the ADVERTISED (gossiped) status, which post-A-1 derives through the shared
  effective-state fn; a no-op verdict is advisory against possible gossip
  staleness — acceptable per the Q3 no-op ruling, and the qualified `id@node`
  path (unchanged) remains the operator's override.
- **Goal-satisfaction semantics, NOT naive verb symmetry** (the mixed case
  breaks pure symmetry): **wake is an ∃-goal** — satisfied when ANY instance
  is Active; **suspend is a ∀-goal** — satisfied when ALL instances are
  Suspended. Unified selection law, one helper parameterized by the verb's
  satisfaction predicate: 0 candidates → NotFound; goal already satisfied →
  NoOp naming the satisfying node(s); exactly 1 actionable instance →
  Act(node); >1 actionable → Ambiguous refusal listing copy-pasteable
  `id@node` candidates.
- Edge rulings this implies: (1) wake with >1 Active = NoOp naming ALL active
  nodes (NOT Ambiguous — nothing is actionable; an ambiguity refusal would
  just defer the same no-op behind a qualified retry); (2) suspend with a mix
  (suspended at X, active at Y) is NOT satisfied (∀-goal) — exactly one
  active ⇒ Act on it; several active ⇒ Ambiguous. Q3's stated wake cases are
  unchanged; this extends them.
- All refusal/no-op/ambiguity strings are user-facing → F-1 language from
  day one. Red-first per selector arm (satisfied-noop, act, ambiguous,
  notfound) on top of the routing red-first above.

**A-4 — op-id collision (`stream-open op already applied … retry with a
fresh op_id`).** Root (high): three counters (`cli-ops.json`,
`pump-ops.json`, `epoch.json`) mint from 1 into ONE broker journal keyspace
`(NET_EFFECT_SESSION, op)` (broker.rs:3021); CLI wake op colliding with an
already-journaled daemon op reproduces the error with NO broker restart. Same
latent class: net-dial (broker.rs:2783), shell-link, sync/update pulls. Fix
(ADR-0034): (i) journal key gains a minter tag (`cli`/`pump`/`psyche`/`epoch`
— F-030 W1's `psyche_ops_epoch_file()` already pre-aligned); old-shape keys
can't collide with new-shape → migration-free; (ii) auto-retry-once with a
fresh op on the typed no-longer-held error, scoped to tracing-only families
(rest, rc attach stream-open); sync/update durable `open_op` semantics
untouched (their dedup is load-bearing). Red-first: mint a CLI op equal to a
journaled daemon op → typed error internally → retry succeeds; user sees
nothing.

**REQ seeds:** `REQ-EFFECTIVE-INSTANCE-STATE` (shared derivation, hazard-class
— A-1), `REQ-WAKE-RESUME-LEG` + `--wait` (A-2), `REQ-REST-VERB-ROUTING`
(A-3), `REQ-OPID-MINTER-NAMESPACE` + `REQ-OPID-TRACING-RETRY` (A-4).

## B — update-apply remote wedge (operator #3)

Three separable fixes (recon: E-#16 successor; local leg shipped, these are
the survivors).

**B-1 — poison-proof the broker floor locks (the wedge itself; top-ranked
root).** Brain-only update keeps broker + QUIC conns + ControllerSink alive
by design (REQ-UPD-3); E-#16 fixed journal poison + loopback bound; the
surviving class is the inject/controller floor Mutexes with bare
`.lock().unwrap()` — broker.rs:1163 (flush_inject_floor), :1297 (inject
worker open), :2142 (buffer_if_held). One panic under a shared floor lock
during attach churn permanently wedges serve/dispatch → every attach
deadlines (`brain IPC read deadline elapsed`, brain.rs:1505) + pump tick
stalls behind the same thread. This also matches the banked B5 churn-pump
observation. Fix: `lock_recover`-style conversion (mirror effect.rs:207).
Red-first: inject_control_wedge.rs — scripted panic-under-floor-lock during
concurrent attach; next attach must still open. KNOWN-HAZARDS entry rider
(brain-only update + poisoned broker-resident lock strands serve/attach).
**B-1 PIVOT (doyle ruling, 2026-07-05, build sweep — 4th triage-drift this
wave):** the 3 named floor sites were ALREADY poison-proofed post-triage
(`lock_floor()` recover helper, REQ-HAZARD-INJECT-WORKER-POISON, red-first
covered) — B-1-as-specified is closed. The SURVIVING members of the same
hazard class (a panic under a broker-resident lock during brain-only-update
churn permanently wedges serve/attach) are the attach/serve/dispatch-path
bare `.lock().unwrap()`s: `sessions` Mutex<HashMap> ×18, per-session
`OutputLog` Mutex ×7, `pair_holds` Mutex ×4. B-1 pivots to poison-proof
ALL THREE via one shared `lock_recover` helper (the inject-floor pattern
generalized). **Ring coherence bind:** `OutputLog`'s ring append mutates
counters — recover-then-serve of a mid-append-torn ring could hand garbage
to a subscriber; the recovery path therefore runs a cheap invariant clamp
(indices in bounds, counts consistent) and on violation RESETS the ring
empty with a loud log — lost scrollback self-heals on the next PTY output
+ repaint; a permanent wedge does not. Sessions/pair_holds are short map
ops (coherent-on-recovery, same argument as the floor/journal). The
REQ-HAZARD-BROKER-FLOOR-LOCK-POISON seed is REDIRECTED to this class in
the same commit (redirect-folded-req-seeds discipline): floor half noted
subsumed by REQ-HAZARD-INJECT-WORKER-POISON, title recast to the
attach-path lock set. Red-first unchanged in spirit: scripted
panic-under-sessions-lock (and a torn-ring variant) during concurrent
attach; next attach must still open and serve sane bytes.

**B-2 — controller-liveness reconcile (stale ONLINE+CONTROLLED).** Root
(certain): control reap gates on `!has_session` (livehost.rs:749,
REQ-PRESENCE-CONTROL-REAP-ON-EXIT); brain-only update keeps the session, so a
SEVERED remote controller's sink is never reaped. Fix: detect a
ControllerSink whose writer/stream is disconnected → `clear_controller`
(broker.rs:635 seam + mark_controller_gone :645), boot-tick + detach-detect
triggers. KH 7.15 guard: reap only — never latch `driven_by` for a local
controller. Red-first: sever a controller stream across a brain restart →
stamps clear within a reconcile tick.
**B-2 ruling addendum (doyle, 2026-07-05, build recon — the triage's "detect
a disconnected writer/stream" was under-specified; four detach paths already
exist):** B-2 is NOT a fifth transport-detach path. The bug class is
**persisted stale stamps**: `controlled`/`driven_by` records that outlive a
severed controller while the session survives (brain-only update, wedge, or
an idle-severed QUIC leg), because the livehost reap arm gates on
`!has_session` and nothing ever re-derives the stamp from broker truth.
Wire-level severance detection stays with the EXISTING paths (conn EOF,
unsubscribe, drain deadline-evict) — B-1's poison fix un-wedged exactly the
cleanup that failed on ball-b, and QUIC keepalive surfaces an idle-severed
conn as an EOF eventually. Fix shape (recorded-truth, Q5 kin):
- Broker gains an authoritative **live-controller query** per session id
  (identity `by` or none). The query handler LAZILY REAPS a sink whose
  writer thread `is_finished()` before answering (catches
  post-keepalive-failure writers without inventing transport probes).
- livehost reconcile's `has_session` arm (which today skips control state
  entirely) queries the broker for every perch stamped
  controlled/`driven_by=Some(remote)`: no live controller or identity
  mismatch → clear the stamps. Boot-tick coverage falls out (reconcile runs
  at boot), which also cleans stamps left by a PRE-restart wedge.
- KH 7.15 binding: reap ONLY, `by=Some(remote)` only — a local `by=None`
  controller is never latched or cleared by this path.
- Red-first: the triage's sever-across-brain-restart int (stamps clear
  within a tick) + a persisted-stale-stamp unit (stamp says controlled,
  broker says none → cleared; broker says same identity → untouched).
**B-2 REDUCTION (doyle-confirmed, 2026-07-05, second recon):** the ruling's
"new query + new reconcile arm" was ~80% already built —
`converge_perch_stamps` (KIND_SESSIONS handler, REQ-HAZARD-CONTROL-STAMP-
CONVERGENCE) already re-derives `driven_by`/`controlled` from broker truth
on EVERY reconcile poll, and livehost already polls per tick. The one gap:
a dead-writer ControllerSink still reports `Some` into the snapshot, so
converge faithfully preserves the stale stamp. **B-2 = the broker-side
lazy-reap only**: in the KIND_SESSIONS snapshot closure, a controller whose
writer `is_finished()` is cleared BEFORE `controller_by` is read; the
existing converge then propagates the honest clear. No new IPC kind, no new
livehost arm. Accepted residual: a truly idle severed controller (writer
blocked on recv, no output, conn not EOF'd) converges only when output
resumes / the conn EOFs — that is the reserved
REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT harder case, deliberately NOT built
here (noted in the REQ impl note). Seed re-scoped to the reduced shape in
the same commit.

**B-3 — rc reconnect loop (the UX the operator asked for).** Today rc is
one-shot: resolve→dial→attach→pump then print-and-exit (rc.rs:1282-1364); no
re-drive exists; FAULT-MATRIX row 9 over-promises. Fix: wrap the sequence in
a loop — on BrokerGone/severed/deadline: clear screen, centered
`Reconnecting to {node}…` (StatusRow owns full-screen paint; remote_node at
rc.rs:1180), re-mint op, re-dial, bounded timeout then plain-language give-up.
Q4 UX rule applies: the reconnect banner and the give-up line speak operator
language; internal detail goes to a debug surface.

**REQ seeds:** `REQ-HAZARD-BROKER-FLOOR-LOCK-POISON` (B-1),
`REQ-CONTROLLER-LIVENESS-REAP` (B-2), `REQ-RC-RECONNECT` (B-3).

## C — Picker cluster (operator #1, #4, #8, #9)

**C-1 — disambiguation misfires (#1).** Root (certain):
`disambiguate_project_ids` counts raw display occurrences, never checks dirs
differ (model.rs:405); `endpoint list` path collides one-dir-per-endpoint
cells with themselves (cli.rs:3089). Fix: count DISTINCT dirs per display
(HashSet); suffix only when >1. Red-first: two refs same display SAME dir →
both bare (data.rs:1026 case block).

**C-2 — remote "Start now" → "Wake now" (#4; ruled).** Today a remote row's
Start bakes `Outcome::Run` with no node → local `cmd_endpoint_run` silently
creates a colliding LOCAL instance of a remote id (model.rs:1197,
mod.rs:360) — violates node-anchored identity (ADR-0003/0023). Remote rows
are only Online/Suspended, so: remote suspended row offers **`Wake now`** →
threads `ep.node` into a new `Outcome::Wake` → `wan_rest(id@node,
REST_EVENT_WAKE)` (plumbing exists, cli.rs:3289); local "Start now" semantics
unchanged; `Instantiate locally` remains the separate documented remote verb.
With A-2's resume leg, remote Wake now actually revives the session — the
full loop the operator wanted. Red-first: remote suspended row → assert no
local `Outcome::Run`; Wake outcome carries the node.
**C-2 ruling addendum (doyle, 2026-07-05, build recon):** (a) triage's
"thread `ep.node`" was written against a lossy display string —
`EndpointRow.node` renders `LABEL (prefix…)` and never re-parses as an
`@node` qualifier; the Wake outcome carries a new RAW `node_key` field
(full hex from the resource row) instead. (b) the same
colliding-local-write class has a TWIN on the same screen: remote suspended
rows also offer ChangeAdapter (offline-gated), whose `write_adapter_change`
mutates a LOCAL perch for the remote id — co-gated to `&& ep.is_local` in
the C-2 commit (+ red test); shipping the Start fix while leaving the twin
open would be half the bug.

**C-3 — `x` purge shortcut (#8).** Key site: handle_pick_key command branch
(mod.rs:251), launch_keys_live gate pattern (F-029 B-1). Purge is
offline-only + node-local (cli.rs:10599, CONTEXT:189) — shortcut inherits
both gates (flash why when gated off). No in-TUI confirm pattern exists →
small `Screen::ConfirmPurge` (B-2 ChangeAdapterPick shape); call
`cmd_endpoint_purge(id, yes=true, force)` — never its stdin `[y/N]` (fights
raw-mode). Refresh + flash `PURGED:{id}` after. Red-first: purge outcome only
for offline local highlight.

**C-4 — Backspace go-back (#9).** `back()` reverse map already complete
(model.rs:1442); pure key alias excluding the two text contexts (CreateId
edit mod.rs:184, filter mode mod.rs:243 — Backspace stays char-delete there;
optional: fall through to back() on empty buffer). Red-first: Backspace on
Confirm → PickExisting; on CreateId → buffer shortens, screen unchanged.

**REQ seeds:** extend `REQ-PICKER-PROJECT-DISPLAY-NAME` (C-1 unit stage),
`REQ-PICKER-REMOTE-WAKE` (C-2), `REQ-PICKER-PURGE-SHORTCUT` (C-3),
`REQ-PICKER-BACK-NAV` (C-4).

## D — Run/resume adapter custody (operator #6, #10)

**D-1 — `run --id` prompts (#6).** Root (certain, no design tension): lone
`--id` falls to picker as create-new prefill (cli.rs:1303); existing-endpoint
case never considered. Fix: `(None, Some(id))` arm reads the perch's
`info.adapter` → non-interactive `cmd_endpoint_run`; missing/adapterless →
today's picker. Red-first: extract pure `resolve_run_target` helper, test the
quadrants.

**D-2 — resume remembers adapter (#10; ruled Q5 @c248afc).**
- `SessionEntry` gains additive `adapter: Option<String>` (exact
  cwd/ordinal serde pattern + back-compat tests, sessions.rs:263 precedent);
  five append sites stamp it (startup.rs:317 has `rec` in scope;
  reporting.rs:100 captures under the rotation lock; digest.rs:601;
  ready.rs:112; livehost psyche ledger).
  **[CENSUS CORRECTION — doyle-confirmed 2026-07-05, D-2 build recon]** the
  five-site list above was triage-era drift; the verified production
  `sessions::append` census is THREE sites: startup.rs:317 (live bind boot
  row), reporting.rs:94 (boundary rotation row, under the mutate_info lock),
  ready.rs:119 (ready-agent boot row, spt-msg). digest.rs:601/612 are inside
  the `cfg(test)` mod (not production); a "livehost psyche ledger" append
  does not exist — a live agent's boundary reaches the ledger via `api
  boundary` → reporting.rs:94 (same append, nothing separate to stamp).
  Third map-incompleteness this wave; the sweep-don't-assume bind caught it.
- Resume picker row shows it: `ResumeRow` gains adapter; title renders
  `{head} [{adapter}] - {time} (…{id5})` (model.rs:213).
- `resume_outcome` bakes the ROW's adapter (today: `ep.adapter_profile`,
  model.rs:1285), None → current stamp (benign degrade).
- Re-stamp rides the **B-2 `mutate_info` seam** (mod.rs:336) pre-spawn —
  NEVER the bind path: the A-4 guard (REQ-HAZARD-ADAPTER-PROFILE-STAMP-CLOBBER)
  keeps governing hook-binds; feature and hazard live on different write
  paths; both existing hazard tests stay green untouched.
- Unregistered recorded adapter → loud pre-spawn refusal naming it +
  `spt adapter add` hint (Q5; F-027 ADAPTER-UNRESOLVED shape).
- Red-first: resume row `adapter="claude-spt"` over endpoint stamped
  `claude-spt:ccs` → stamp becomes `claude-spt` (the deliberate replace).
- NOTE: A-2's wake-resume leg consumes the same ledger field — land D-2's
  schema chunk BEFORE A-2's resume leg.

**REQ seeds:** `REQ-RUN-ID-REUSES-ADAPTER` (D-1),
`REQ-SESSION-ADAPTER-RECORDED` + `REQ-RESUME-ADAPTER-FOLLOWS-SESSION` (D-2).

## E — Messaging + title singles

**E-1 — from-stamp `cli@NODE` (#7).** Operator-corrected topology: ball-b is
spt-hosted (broker PTY, headless, remotely controlled), v0.24.0 confirmed —
so the v0.22.0 ancestry fix's surviving hole is the root: self-detect leg (c)
matches `rec.pid` only (roster.rs:153) — for an spt-hosted endpoint that pid
is the ephemeral bind-CLI pid, ALREADY DEAD (the F-026 #11 dead-pid class,
sighted on hall-bf today) — never in any sender's ancestry. `rec.parent_pid`
holds the harness pid (startup.rs:237), IS the ancestor, and CONTEXT names it
"the stable session-binding anchor". Fix: leg (c) also matches
`rec.parent_pid` (label-only, auth untouched — KH 7.3/7.5 separation holds).
Env legs stay first (raw-PS7 env non-propagation explains the direction
asymmetry). Red-first int (currently missing, specified): `rec.pid` dead
sibling + `rec.parent_pid` genuine ancestor → self resolves. Rider: #11
dead-pid itself (rec.pid should hold something that stays true, or liveness
readers stop trusting it) — same cluster, activate together.

**E-2 — rc title replay (#2).** Root (certain): `GridState` has no
`osc_dispatch` (vte default drops OSC 0/2) and `render_repaint` emits no
title (screen.rs:687/571) — cold attach paints a perfect screen with a stale
title. Fix: `title: Option<String>` on the grid, capture OSC 0/2, clear on
RIS, emit `\x1b]2;{title}\x07` in repaint; flows through repaint_initial
automatically (broker.rs:463). ADR-0031 one-line amendment rider (repaint
also carries the last title). Red-first: feed title + text → repaint contains
both (screen.rs:898 pattern); attach.rs round-trip.

**REQ seeds:** `REQ-SELF-DETECT-PARENT-PID` + dead-pid rider (E-1),
extend the ADR-0031 grid REQ with title stage (E-2).

## F — Cross-cutting riders

- **F-1 public-error-surface sweep (Q4 UX rule, operator-ruled):** CLI stderr
  a non-developer can hit must name the observable situation + next action —
  never journal/op/brain lingo. Known offenders: the op-id retry text (dies
  inside ADR-0034's retry), `brain IPC read deadline elapsed` (parenthetical
  lingo in a user-facing line, rc.rs:1222), `WOKE_FAIL … not a hosted perch`.
  Kin to banked patterns: public --help no internal codes; "Updated" not
  "trial"; translation_fault not human-rendered (that seed rides here too:
  render translation_fault + psyche_host_error in `list`/`whoami`).
- **F-2 relay-unregister casualty (C-2 follow-up (a), field-repro'd on
  hall-bf 2026-07-04):** post-clear, owl-path send → NO_PERCH while ready
  present + inject path healthy; clear's soft session-end unregisters the
  relay address, boundary re-stamps ready but never re-registers. Fix rides
  the same boundary seam as F-029 C-2 (reporting.rs cmd_boundary) — priority
  bumped by field repro.
- **F-3 FAULT-MATRIX row 9** made true by B-3 (rc re-drive) instead of edited
  down.
- **F-4 docs:** KNOWN-HAZARDS entries (B-1 floor-lock poison; A-1 effective
  instance state as hazard-class), ADR-0031 amendment (E-2), ADR-0033/0034
  land with this doc.
- **F-5 adapter floor NOT enforced on acquisition (perri finding, public-repro
  2026-07-04, doyle source-confirmed on the 0.25.0 tree):** `spt adapter add
  --release` + `spt adapter update` install/activate an adapter whose
  `min_spt_core_version` exceeds the running core — clean, ACTIVE, zero
  warning (negative test: fresh SPT_HOME + 0.24.0 binary + claude-spt v0.14.0
  floor "0.25.0"). Source: the floor field is parsed + required and
  `[update].version_check` is documented "Verify spt-core satisfies
  min_spt_core_version before/after" — but no code path compares the floor to
  the running core version anywhere; `version_check` is dead-parsed (fixtures
  only). This is the v0.13.2 silent-starve class reachable via the documented
  one-command update. Fix shape: enforce on BOTH acquisition verbs (refuse
  with a public-language error naming core version, floor, and the next
  action; no core lingo per F-1's rule), and either honor `version_check`
  semantics or retire the field — a doc'd-but-dead knob is a contract lie.
  **[Rulings, doyle 2026-07-05]** `version_check` RETIRED (drop field +
  schema + docs sweep). ADD gate = inside `registry::register` (single choke
  point, pre-any-write; `register_with_core(core_version)` for test
  injection; typed CoreFloor refusal, F-1 Display). UPDATE gate = **(a)
  pre-swap temp-extract peek**: parse the staged archive's manifest floor in
  a throwaway temp BEFORE the CRC-swap — register-time refusal is TOO LATE
  for a pointer-mode adapter (the swap has already replaced the live files;
  record refuses but the home is corrupted = the silent-starve class again,
  one layer down). "Nothing installs, registry untouched" binds BOTH verbs.
  Semver-lite numeric-per-component compare (`version_meets_floor`), no new
  dep.
- **F-6 post-v0.25.0 cleanup riders (doyle signoff seeds, 2026-07-05):**
  c1_miss_once flake-harden (load-sensitive under full-parallel, hit 3x); CI
  `test` job should exclude seedmap like the gates (`not test(/seedmap/)`);
  docs-site/book/ gitignore-tracked HTML hygiene (`git rm --cached`);
  MANIFEST.md:126 clarify PSYCHE_RESEED = operator-log-only, not an
  adapter-facing signal; `owl send --reply-to <r> <target>` with [FROM]
  omitted stamps the envelope `from=<r>` (the reply-to value) instead of the
  auto-detected sender — self-addressed attribution mangle, field-observed
  doyle→perri 2026-07-05 (from should auto-detect or the verb should refuse).

## Sequencing sketch (chunks, doyle gates per sha)

1. **A-cluster first** (A-1 derivation → A-4 namespacing → A-3 routing →
   A-2 resume leg; D-2's schema chunk lands before A-2). The wave heart.
2. **B-1** early (field wedge, smallest diff, biggest pain); B-2, B-3 follow.
3. **C/D/E** parallel-safe smalls in any order; C-2 after A-2 (wake-now needs
   the resume leg to be honest).
4. **F-1 sweep** last (touches strings across surfaces; do once the error
   sites have settled).
- Cross-node legs (A-3, A-4, B-2, B-3, C-2, E-1) need the two-node rig →
  `[twohost]` YES for this milestone's wave-final ints; singles stay local.
- F-030 runs beside on its own branch; the two waves touch different seams
  except livehost — coordinate the A-2 resume leg with F-030 W3's residency
  retirement (both edit reconcile; A-2 SHOULD land after W3 merges or rebase
  onto it).
