# NEXT MILESTONE — PICKER POLISH (F-029), operator triage 2026-07-03 post-v0.23.0

JIT plan authored by doyle (diagnoses code-anchored, `/diagnose` discipline; fixes
NOT applied — this doc is the wave's work order). Operator evidence: screenshot of
the Confirm panel showing raw slugs + bare adapter name (ShareX
`WindowsTerminal_7FkRXDFrRe.png`, 2026-07-03).

Gate rules unchanged: red-first per item where testable, both CI legs, doyle
worktree gate per sha, traceable per-wave activation (new REQs registered first,
`required_stages=[]` until the item starts).

---

## Part A — v0.23.0 bugs (diagnosed to root)

### A-1. Confirm-panel project history renders RAW slugs (A1-display gap)
**Evidence:** `Project history: github-com-sabermage-spt-core, projects` in the
"Confirm selection" top panel.
**Root (pinned):** `picker/view.rs:415-419` `render_selection_summary` builds the
line from `ep.project_history.join(", ")` — the field documented as "RAW project
IDs" (`model.rs:419`). The v0.22.0 A1 display-name machinery
(`disambiguate_project_ids` over `project_refs[].display`,
REQ-PICKER-PROJECT-DISPLAY-NAME) was wired into the choose-project rows and list
column but never this render site.
**Fix shape:** render `disambiguate_project_ids(&ep.project_refs).join(", ")`
(same collision-suffix semantics everywhere). Unit seam: view.rs summary tests
(`render_selection_summary` already unit-rendered off-tty).
**REQ seed:** extend REQ-PICKER-PROJECT-DISPLAY-NAME with a unit on this site
(red-first: raw-slug assert fails on the fixed render).

### A-2. History row lacks "(CURRENT DIR)" marker when run cwd ∈ history
**Evidence + operator ruling:** hiding the `Here:` row when the current dir is
already a history entry is CORRECT (REQ-PICKER-CHOOSE-DEDUP-ALL holds); but the
matching history row must self-identify.
**Root (pinned):** `picker/model.rs:322-352` `build_project_choices` — the dedup
at :339 suppresses the `Here:` row, and the history rows (:327-331 head, :345-350
rest) render bare `r.display` with no cwd affordance.
**Fix shape:** when `r.dir == run_cwd` (non-empty), label =
`format!("{} (CURRENT DIR)", display)`. Pure fn, unit-complete (extend the
:1867-1889 test cluster).

### A-3. "Here: " label → "CURRENT DIR --> <project>"
**Operator ruling (semantic pairing with A-2):** the not-in-history current-dir
row changes from `Here: <run_cwd>` to `CURRENT DIR --> <project>`.
**Anchor:** `picker/model.rs:341`.
**Fix shape:** derive the display for `run_cwd` with the SAME source-derivation
the history refs use (folder tail; honest fallback to the path when underivable):
`format!("CURRENT DIR --> {display}")`. Keep `cwd` payload unchanged. Update the
`starts_with("Here: ")` test asserts (3 sites :1876-1889) — they are behavior
assertions on the OLD label; grep-tests rule applies.

### A-4. REGRESSION — picker harness views drop the adapter `:profile`
**Evidence:** Confirm panel shows `claude-spt` where an `<adapter>:<profile>`
endpoint previously showed the full option (harness pane test
`view.rs:760` asserts `claude-spt:fast` — the RENDER is profile-capable).
**Root (pinned):** the render reads `info.json.adapter` verbatim
(`picker/data.rs:192-195`). The CREATE path stamps the FULL option
(`cli.rs:2025` in `resolve_home_and_write_skeleton`, receiving `cmd_endpoint_run`'s
`<adapter>[:profile]` verbatim). The CLOBBER is the BIND path:
`bind_from_seed`/`establish_perch` (api/startup.rs) resolves the adapter
ADAPTER-AGNOSTICALLY at bind (ADR-0021 basename/pointer resolution → BASE name,
profile unknowable from a binary basename) and `stamp_creation_fields`
(spt-store/home.rs:133) gives the incoming bind-time value PRECEDENCE:
`rec.adapter = adapter.map(...).or_else(|| p.adapter.clone())` — so the first
hook bind rewrites `claude-spt:ccs` → `claude-spt`. (F-028's establish_perch
self-heal widened how often this path re-stamps; the precedence bug is the root.)
**Fix shape (profile-preserving precedence):** in `stamp_creation_fields`'s
`Some(prior)` arm, when the incoming `adapter` equals the PARENT of
`prior.adapter` (helper exists: `spt_runtime::profile::adapter_parent_matches`,
profile.rs:71 — note crate-boundary: either move/duplicate the trivial
parent-split into spt-store or compare on the caller side), KEEP the prior's
richer `parent:profile` value; replace only on a genuinely different adapter.
Red-first unit: prior=`claude-spt:ccs` + incoming=`claude-spt` → stays
`claude-spt:ccs`; prior=`claude-spt:ccs` + incoming=`other-adapter` → replaced.
**REQ seed:** REQ-PICKER-ADAPTER-PROFILE-PERSIST (or fold under a
REQ-BIND-ADAPTER-STAMP-PRECEDENCE hazard — todlando's call at registration).

---

## Part B — operator change set (rulings inline)

### B-1. Restrict `h` (headless) / `s` (shortcut) to launch-capable highlights
Today `h`/`s` fire from broad picker contexts (`mod.rs:184,192,234`). Restrict to
highlights that would LAUNCH the endpoint:
- "Start now" when the endpoint has no project history other than the current dir
  (i.e. the immediate-start case of `should_offer_project_choice == false`);
- a "Choose project" row (post-"Start now");
- a "Resume from a prior session" row (post-"Resume from history").
Implementation: gate the key handlers on (screen, highlighted-option) rather than
screen alone; the footer hint line must render `h`/`s` ONLY when live (hint truth
= availability truth). Unit the gate as a pure (screen, option) → bool matrix.

### B-2. Fix "Change harness adapter" flow
Today `ConfirmOption::ChangeAdapter` (mod.rs:259) routes into the CREATE flow
(`Screen::CreateAdapter` → CreateId → CreateHome → START, model.rs:1282) — it
re-prompts id + home and then STARTS the endpoint. Required: prompt ONLY the
harness-adapter pick, apply the change to the perch record (update
`info.json.adapter` via the existing write seam; preserve/reset profile per pick
— an `<adapter>:<profile>` pick stamps the full option, composing with A-4), then
RETURN to the endpoint's Confirm menu (no id prompt, no home prompt, no start).
Implementation: a return-to-Confirm mode on the adapter-pick screen (flag or
dedicated `Screen::ChangeAdapterPick`), skipping CreateId/CreateHome and the
launch outcome. Unit: flow-state transitions + the record write.

### B-3. "Fork endpoint" label → "Fork endpoint here --> <current dir>"
Anchor `view.rs:387` (`ConfirmOption::Fork` label fn). Dynamic label needs the
run cwd threaded to the label site (labels fn is currently static str — make it
`fn(model) -> String` for the dynamic rows or special-case at render). Honest
truth-surface: the label must state the dir the fork ACTUALLY lands in (verify
against the fork outcome's cwd resolution before wording).

### B-4. Shortcut option label → dynamic
`view.rs:388` static `"New/Update spt-<id> shortcut (s)"` →
`Set shortcut here --> <current dir>/<shortcut-name>` where `<shortcut-name>` is
derived by the SAME function that names the file in shortcut creation
(`picker/shortcut.rs` — `<basename>-<id>` with adapter-overridable basename,
DEFAULT_BASENAME `spt`). Factor the name derivation so label and writer share one
fn (no drift possible); label renders its real output filename incl. extension
exactly as written to disk.

### B-5. `spt endpoint run` window/tab title → `SPT Endpoint Picker`
Anchor: `picker/mod.rs:88` `setup_terminal` — add crossterm
`SetTitle("SPT Endpoint Picker")` to the `execute!` chain (and consider restoring
the prior title on exit if cheaply capturable; if not, set-only is acceptable —
note in the commit). Applies to the interactive picker path of `endpoint run`
(non-interactive/headless invocations must NOT retitle the operator's terminal).

---

## Wave order (doyle-proposed; amended 2026-07-03 late — B6 joins W2 by operator ruling)
1. **W1 label/render cluster (pure, unit-only):** A-1, A-2, A-3, B-3, B-4 —
   all in the picker's pure render/choice-building layer; one chunk gate.
2. **W2 behavior cluster:** A-4 (cross-crate precedence fix + red-first),
   B-1 (key gating matrix), B-2 (flow rework — the largest item), B-5 (title),
   **C-1 translation fault-respawn (B6 root fix — see addendum below)**.
3. Wave-final: int consideration — A-4 has a real-daemon seam
   (bind-over-created-perch preserves profile); C-1's fault-respawn int is
   REQUIRED (paid-for field bug, conformance-grade). Others unit-grade-pure.
   [twohost] NO (single-node, display/flow + daemon-local lifecycle only).

---

## Addendum (doyle, 2026-07-03 late) — C-1: B6 ROOT PINNED, fix rolled into W2

**Root cause (code-pinned + perri captured-stderr proof, iso 0.23.0 daemon):**
the checkpoint ARM's clear-only inject drives `/clear`; the `{commit}` for that
sequence is never observed within `INJECT_COMMIT_DEADLINE` (5 s, broker.rs:158);
the inject worker then FAULTS the binary — flush+release floor, respool the
envelope (broker.rs:1394 `respool_and_fault`), **terminate the translate child,
latch `Translation.faulted`, worker exits** — and by DESIGN **never respawns**
(broker.rs:152-157, ADR-0022 amendment). From that instant every force-native
reports `delivered=false` → the CLI's generic `FORCE_NATIVE_UNDELIVERED: no
live translation binary` (cli.rs:5046) while the translate *process* the
operator checks is the terminated one's successor-less absence — B6's exact
field signature. The v0.12.0 checkpoint post-clear WAKE dies twice over: the
armed in-memory `pending_wake` perishes with the terminated binary AND the
SessionStart FIRE lands in the dead window, fire-and-forget, unchecked.
**B6's "intermittency" was never a race** — it is deterministic at every
checkpoint-armed boundary; apparent recovery = session respawn/adapter-apply
rebuilds (`build_translation` sites broker.rs:2055/2925 — the ONLY two).
Perri's trace: `TRANSLATION_FAULT: no {commit} within INJECT_COMMIT_DEADLINE —
terminating binary` on the captured daemon stderr (the F-019 unread channel,
which is why three versions never saw it).

**C-1 fix shape (core; ADR-0022 amendment REQUIRED — REVISED after
operator/perri pushback, supersedes the earlier terminate-then-eager-respawn
shape):** a deadline miss is NOT a fault — **preserve the binary.** The
terminate-then-respawn shape was retracted because it guarantees the loss of
the binary's in-memory armed wake, forcing the wake text into a holdover file
(the rejected b1 hazard, F-024 env-file-carrier lesson class). The watchdog's
legitimate job is ANTI-STALL (release the operator-input floor), not
execution-verification; terminating a healthy binary over one eaten `{commit}`
is the whole bug.

1. **Miss ≠ fault:** on a commit-deadline miss, keep today's flush+release of
   the floor (anti-stall unchanged, broker.rs:1392) and the envelope respool
   (:1395) — but the worker **`continue`s to the next queued event instead of
   `return`ing** (:1402 today), the binary is NOT terminated, `faulted` is NOT
   latched. The in-memory `pending_wake` survives the boundary → the original
   v0.12.0 wake design works as intended, zero adapter change, zero holdover
   file. The loop-top stray-command drop (:1337) already neutralizes a LATE
   `{commit}` from the missed sequence; note the residual mid-drain
   mis-attribution window (old sequence's dribbling commands during the next
   event's drain) — todlando picks the drain discipline, the existing
   stray-drop is likely sufficient (binaries emit fast).
2. **Strike budget for REAL wedges:** N CONSECUTIVE misses with no intervening
   successful commit (N=3 proposed) → then fault as today (terminate + latch +
   respool). A commit resets the counter. This preserves the wedged-binary
   protection the never-respawn rule existed for, bounded at N×deadline of
   worst-case stall exposure (floor still releases every 5 s regardless).
3. **Bounded respawn on REAL fault:** binary-death faults (stdin write fail
   :1343, stdout disconnect) and strike-exhaustion faults get a bounded eager
   respawn (C3(b) give-up budget) instead of permanent death — this half of
   the old shape survives; it just no longer carries the wake (a real fault
   legitimately loses in-memory state).
4. **Perch-visible fault surface:** real faults stamp the perch record
   (mutate_info field, cleared on healthy respawn/commit) — `TRANSLATION_FAULT`
   today is daemon-stderr-only (the F-019 trap; same honesty rule as F-027's
   ENDPOINT_SPAWN_FAIL).

**Red-first:** int via mock translate that misses `{commit}` ONCE
(`SPT_INJECT_COMMIT_DEADLINE_MS` shrink, existing Layer-G rig) → assert binary
NOT terminated (same child pid), floor released, envelope respooled, and the
NEXT inject delivers through the SAME binary — RED on 0.23.0 (worker exits,
faulted latches) by construction. Second int: N consecutive misses → fault +
terminate + perch stamp + bounded respawn; budget exhaustion stays dead+loud.
Wake-survival e2e (ARM → clear → FIRE → wake emitted) rides adapter-side once
perri wires her regression harness (wake-repro.sh pattern).

**Traceability:** rescope open `REQ-TRANSLATE-BINARY-LIVENESS-DECAY` →
`REQ-TRANSLATE-COMMIT-MISS-TOLERANCE` (+ hazard
`REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH` — paid-for: B6 + three-version
checkpoint-wake breakage). Register before starting, per-wave activation.

**Adapter-side (perri, independent):** b3 RETRACTED (needs the holdover file).
Remaining: loud-fail surface on an undelivered FIRE (regression visibility,
cheap) + her binary-side look at WHY the `{commit}` was eaten during the
clear it drove (core fix is independent of the answer, but the answer decides
whether a commit-less-inject protocol variant seed stays parked or dies).

**C-1 scope addendum (doyle, post-chunk-1) — receive-side pin pending + DOCS
deliverables fold in:**
- **PIN LANDED (perri discriminants, artifact-grade): (B) — the zero-record
  response.** Her binary's FIRE-with-nothing-armed branch returns zero records
  and NO bare `{commit}` (her translate.rs:280; live-verified zero bytes);
  the SessionStart hook self-sends `checkpoint_fire` on EVERY clear (stateless
  by design), so every non-checkpoint `/clear` and every duplicate fire = a
  5 s silent wait → fault → terminate. Iso stderr proof: the ARM's commit was
  ACCEPTED (no fault after it); the fault followed the DUPLICATE fire. The
  "eaten commit" attribution is DEAD. (A) EXCLUDED — all her sequences total
  ≤~150 ms. (C) EXCLUDED — zero `TRANSLATION_BIN_BAD_LINE`; commit line bytes
  verified pure-ASCII `{"commit":true}\n`.
- **C-1 UNHELD — final shape confirmed + two additions:**
  1. Deadline semantics UNCHANGED (whole-sequence 5 s is fine — (A) excluded).
  2. miss≠fault + N=3 strike budget + bounded respawn on real fault + perch
     stamp: all stand as written.
  3. **NEW — respool-once (dead-letter):** under miss≠fault, a respooled
     zero-record envelope (the duplicate FIRE) redelivers on a later drain →
     misses again → respools again = an INFINITE cycle of 5 s floor-holds and
     strike churn. A missed envelope respools AT MOST ONCE; a second miss
     dead-letters it LOUDLY (stderr + counts toward the perch fault surface,
     never a silent drop — REQ-HAZARD-IDLE-SILENT-NONDELIVERY posture: this is
     a reported non-delivery, not an ephemeral evaporation).
  4. Docs item 3 CONFIRMED: both manifest docs gain the explicit rule — **a
     binary MUST answer every `{type:event}` with at least a bare
     `{"commit":true}`** (an empty response is a protocol violation the
     deadline treats as a miss).
- **SEAM-2 (distinct, still OPEN — the box repro is NOT explained by (B)):**
  on the busy box, post-clear force-native went UNDELIVERED **immediately**
  (t+~2 s — no 5 s window, so not the fault path) and never recovered
  (stalled pump, no reconcile). A boundary registration/gate loss on a
  long-running multi-endpoint daemon is real and separate. Field
  discrimination is currently impossible because `FORCE_NATIVE_UNDELIVERED`
  prints the SAME message for every falsy leg (CLI gate refused / broker
  no-session / broker spooled) — **C-1 hardening rider: differentiate the
  UNDELIVERED stderr by leg** (gate-refused vs no-session vs binary-spooled),
  so the next box repro pins SEAM-2 from the CLI output alone. SEAM-2 root
  hunt continues after C-1 lands.
- Adapter half (perri, v0.13.1, ready): fire-with-nothing-armed answers a bare
  `{"commit":true}` (ditto event-without-envelope). Kills the every-clear
  fault adapter-side regardless of core grace; composes with C-1 either
  ordering. Cut on operator go.
- **Docs deliverables (ride the C-1 chunk, same commit set as the ADR-0022
  amendment — verified drift, code-truth vs docs):**
  1. `docs-site/src/harness-contract/manifest.md:323` (PUBLISHED contract):
     the missed-commit consequence reads "falls back to a raw inject —
     delivery degraded" — STALE since the v0.14.3 raw-inject removal; the
     v0.23.0 truth is fault → TERMINATE (permanent until session restart).
     Rewrite to describe the NEW C-1 miss-tolerance semantics (that's the
     behavior the next docs-publish ships with).
  2. `docs/MANIFEST.md:329` (internal contract doc): the stdout vocabulary
     OMITS `{"commit":true}` entirely — no terminator requirement, no
     deadline. Sync to the published wording + C-1 semantics (internal doc
     lagged the published one; ADR-0014 doc-truth-lives-here demands parity).
  3. If (B) pins: add the explicit "empty response = bare `{commit}`" rule to
     BOTH files.
- Provenance notes: legacy claude_skill_owl has NO translate/commit protocol
  (the contract is an spt-core ADR-0022 invention — no legacy semantics for
  the adapter to have inherited); the `{commit}` variant comment
  (translation.rs:73, "doyle OPT-1 ruling, pending confirm") predates the
  published-doc consequence drift.

## Addendum (doyle, 2026-07-04) — C-2: SEAM-2 PINNED, late pre-RC chunk

**Root (perri wakep9 gate-state dump vs wakep4 healthy control + doyle code
trace):** at every UNDELIVERED instant exactly ONE gate field differs — the
**`ready` marker is ABSENT** (info.json online/controllable/rotated-sid all
healthy, translate alive). Mechanism: at a `/clear`, CC fires **SessionEnd
(reason=clear) for the departing session BEFORE SessionStart**; the departing
sid still matches the perch pin at that instant, so the adapter's
`[hooks.SessionEnd] → api session-end` **authenticates** and the soft handler
removes the ready file (+ unregisters any relay address, reporting.rs:206-207).
The subsequent boundary rotates the sid but NOTHING re-writes ready →
`is_online` false → `try_spt_hosted_inject` Nones out on the CLI gate before
any broker RPC (matches ask-#2's zero-inject-line prediction) → every
force-native (including the checkpoint FIRE) reports the generic UNDELIVERED.
Persistent by construction: no path re-stamps ready outside a real bind.
(The pre-0.13.1 "iso ~20s heal" datapoint is retro-attributed to the (B)-era
queue-accept illusion — on the pure seam nothing heals, box and iso alike.)

**C-2 fix shape (core, surgical):** `cmd_boundary` **re-stamps the ready
marker (+ status online, idempotent) atomically with the sid rotation** — a
boundary PROVES a live successor session on the same harness process, so
readiness is truth, not a favor. A REAL session end has no subsequent
boundary, so genuine teardown semantics are untouched. Heals the strand
regardless of which pre-boundary path removed the marker. Red-first int:
perch + ready + soft session-end (ready gone) + boundary → ready PRESENT +
`is_spt_hosted_no_relay` true again; RED on 0.23.0 (boundary leaves ready
absent). Traceable: mint `REQ-HAZARD-BOUNDARY-READY-STRAND` (hazard,
paid-for: SEAM-2 = B6's second half, the live wake blocker) — register first.

**Follow-up seeds (NOT this chunk):** (a) the same session-end also
`unregister_address`es a HARNESS-HOSTED endpoint's relay while its listen
loop still runs — second casualty of the same root, needs a field repro to
size (perri cross-ref: matches the F-023-era "WAN spool-only after /clear"
report shape — check that ledger when this seeds); (b) adapter-side: fire `session-end` only on genuine ends (CC provides
the reason field; skip clear/compact) — semantic fix, unnecessary once C-2
lands but hygienic; (c) an owner-alive guard inside soft session-end
(skip ready-removal when the recorded owner pid is alive) — broader belt,
evaluate after C-2 field data.

## Standing context for the builder
- v0.23.0 counter 43 + adapter claude-spt v0.13.0 published + gated 2026-07-03.
- B6 (translation liveness decay) REQ stays OPEN — boundary-race candidate,
  capture rig proven; NOT part of this wave.
- Backlog seeds NOT in this wave: churn-resilient pump (B5), rest_state void,
  abandoned-controller eviction live-session leg, D1/D2, SPT_HOME docs-gap,
  F-027 (doyle design pending).
- Grep-tests rule: A-3 changes an asserted label — sweep tests for "Here: "
  asserts BEFORE calling green. Shared-seam rule: B-2 touches picker flow state —
  run the full picker test cluster.
