# Phase 25.2: Doyle cluster fix candidates — blast radius sanity check across SPT listener wrapper and tracked-repo paths - Context

**Gathered:** 2026-05-22
**Status:** Ready for planning

<domain>
## Phase Boundary

Five blast-radius-audited fix candidates surfaced from the doyle listener-poll diagnostic session (2026-05-22). Phase scope is **5 candidates, not 6** — original #6 (haiku signoff-absorption compression) is dropped at user direction (meant to be flagged as ignoreable; got rolled into the phase by mistake).

The five in scope:

1. **#1 Stale `index.lock` blocks sessions-log seal** — `src/common/tracked.rs:362-366` `ensure_worktree` fast-path needs a stale-lock probe + best-effort remove. Blocks every `commit_agent_payload` / `commit_project_payload` / `git_commit_context` caller.
2. **#2 Ghost `tracked/.git/` Phase 23-era repo** — Cosmetic noise. Cleanup script + idempotent guard in `migrate_legacy_if_needed` (`src/common/tracked.rs:1458`).
3. **#3 wrapper-state.json 2000ms timeout warning** — Cold `claude -p` exceeds the listener's 8×250ms retry budget. Affects every `read_wrapper_state` consumer (boot, signoff, commune, pulse, init triggers).
4. **#4 Wrapper poll-sentinel uses flat `ready_file()`; psyche perch now nested** — Phase 25 D-16 moved the psyche perch to `owlery/<self>/nested/<self>-psyche/`. `src/live/wrapper/mod.rs:629` (and sibling `wrapper/` callers of `ready_file`, `perch_dir`, `info_file`, `inbox_dir`) still hit flat paths. Wrapper exits poll iter 1 every spawn. **CRITICAL blast radius** — orphan detection, `drain_stale_init_signoffs`, `read_wrapper_state` writer, `$LIVE list/status`, SessionStart hook `session-resume` parent_pid matching, binary handoff (Phase 18.4/18.5 `wrapper-state.json` rehydrate), worker perches.
5. **#5 `drain_stale_signoff_file` discards fresh signoffs** — `src/live/start.rs:138-178` surfaces body to stdout then DELETES `.claude/<id>-signoff.md`. Cannot tell stale-prior-gen from fresh-user-written-while-offline. Payload-loss confirmed in doyle case. Coexists with wrapper's own `drain_stale_init_signoffs` (`src/live/wrapper/lifecycle.rs:116`) — both touch signoff-shaped envelopes, must not double-eat.

**Dropped:** #6 haiku signoff-absorption (out of scope per user — psyche.md prompt is fine as-is; perceived compression was a Psyche-LLM content judgment, not a structural defect).

**Cross-cutting integration mandate (from ROADMAP):**
- #1+#3+#4 = one coordinated wrapper-path-correctness sweep (shared root: flat-vs-nested + wrapper-state read/write paths).
- #5 = standalone signoff-payload-preservation fix.
- #2 = standalone cleanup.

</domain>

<decisions>
## Implementation Decisions

### Plan Granularity & Wave Structure

- **D-01: 3 plans matching ROADMAP groups.**
  - **Plan 1** (`25.2-01`): #1 stale-lock probe + #3 wrapper-state read race + #4 flat→nested wrapper-path sweep. One coordinated wrapper-path-correctness plan.
  - **Plan 2** (`25.2-02`): #5 deliver-then-die (forward latent signoff as commune). Standalone after #6 was dropped from scope.
  - **Plan 3** (`25.2-03`): #2 ghost `tracked/.git/` cleanup. Standalone.

- **D-02: Parallel waves — all 3 plans independent.** No file overlap. Wrapper-path sweep touches `src/common/tracked.rs`, `src/live/wrapper/*`, `src/live/start.rs`, `src/live/signoff.rs`. DTD touches `src/live/start.rs` (`drain_stale_signoff_file`) plus a new commune-send call site. Ghost cleanup touches `src/common/tracked.rs::migrate_legacy_if_needed` only. Caveat: Plan 1 and Plan 2 both touch `src/live/start.rs` — keep edits in disjoint regions; merge-order them only if collision surfaces during execution.

- **D-03: Audit deliverables inline in each plan's task 1.** No separate AUDIT.md sibling doc. Plan 1 task 1 enumerates every flat-vs-nested call site (wrapper/mod.rs:629 + every other `ready_file` / `perch_dir` / `info_file` / `inbox_dir` caller under `src/live/wrapper/`). Plan 2 task 1 enumerates signoff drain consumers (`drain_stale_signoff_file`, `drain_stale_init_signoffs`, `scan_drop_files`). Plan 3 task 1 enumerates ghost-repo readers (confirm none in production code).

- **D-04: Verification = operator UAT + targeted integration tests per candidate.** Each plan ends with: (a) one operator smoke checkpoint (revive an affected agent; observe seal/sessions row land; confirm no warning regression) and (b) 1–2 integration tests per candidate (e.g., golden fixture for #1 stale-lock probe, file-write-before-delete behavior for #5, path-resolution for #4).

### #3 Wrapper-State Read Race

- **D-05: Raise `MAX_ATTEMPTS` from 8 (2s) to 80 (20s).** Matches observed cold `claude -p` p99 latency window (5–15s). Simplest mechanism. No protocol churn. Normal warm-cache reads still resolve in <1s (fewer attempts used; budget is a ceiling, not a floor).

- **D-06: Single shared const `WRAPPER_STATE_MAX_ATTEMPTS = 80` everywhere.** Live in `src/live/wrapper_state.rs` (or `src/common/`). All consumers import — boot (`start.rs:592–630 emit_boot_trigger_after_spawn`), signoff (`signoff.rs:225-228`), commune trigger, pulse trigger, init trigger. Drift impossible.

- **D-07: On exhaustion (>20s no UUID), skip the row + structured warning.** Preserves current behavior — no abort. Boot/commune/signoff trigger logs `WARNING: ...` with elapsed-ms and continues. Phase 30 invariant preserved (signoff content already written before this code runs).

- **D-08: Log `wrapper-state read: <elapsed-ms> (boot|signoff|commune|pulse|init)` on every successful read.** Cheap observability that surfaces drift before next outage. Operator can grep daemon.log / spt logs. Optionally suppress when elapsed_ms < 50 if log noise becomes an issue (defer to plan).

### #5 Deliver-Then-Die Contract

- **D-09: Forward rescued signoff body as a regular commune, NOT as INIT_SIGNOFF.** INIT_SIGNOFF triggers wrapper shutdown (`<init_signoff>` handler in psyche.md line 238: "your wrapper will exit"). If `drain_stale_signoff_file` queued INIT_SIGNOFF, the wrapper would die mid-boot AND any subsequent commune would land on a dead/confused wrapper. Commune envelope keeps the wrapper alive and lets Psyche absorb the prior-session brief as context.

- **D-10: TCP-first delivery with spool fallback.** Mirror existing `$LIVE commune` semantics. Try TCP send to psyche perch first; on NO_PERCH / offline, spool a commune envelope. Wrapper picks it up via the normal poll path. Delete `.claude/<id>-signoff.md` only after queue confirmed (deliver-then-die — never delete before the body has a durable destination).

- **D-11: Envelope = `<EVENT type="latent signoff" ...>...</EVENT>`.** Match the existing commune / echo-commune EVENT convention (Phase 23 D-04+ envelope shape). Body wrapped, source attributes (`written_at`, `cleared_from`) preserved. Psyche absorbs as informational context (no special handler needed — falls under existing "absorb silently" rule in psyche.md:52 for non-keyword envelopes). Naming: **`latent signoff`** (renamed from "rescued signoff" during discussion — "latent" better captures the semantics: pending body that sat dormant until $LIVE start surfaced it).

- **D-12: No coordination needed with `drain_stale_init_signoffs`.** Wrapper's drain matches the `is_init_signoff_envelope` predicate ONLY. `<EVENT type="latent signoff">` is a different envelope shape and is correctly ignored by the wrapper-side drain. Documented as an invariant in Plan 2 task 1 (audit step).

### Claude's Discretion

- Exact wording / line numbers for the per-plan audit task 1 deliverables — planner can shape these from the blast-radius bullets in ROADMAP §639 and the debug doc.
- Test names + golden fixture layout for #1 stale-lock probe (suggest `tests/golden_tracked_stale_lock.rs` or similar; planner picks).
- Whether to introduce a shared helper for the new EVENT-envelope build in #5 vs inlining at the `drain_stale_signoff_file` call site (helper preferable if there's an existing envelope-builder utility to reuse; planner inspects `src/common/event.rs` or equivalent).
- Whether `D-08` elapsed-ms logging gets a threshold floor (e.g., skip log if <50ms) — defer to plan based on observed log volume.
- For Plan 3 (#2 ghost cleanup): whether the cleanup runs unconditionally on every `migrate_legacy_if_needed` call or behind a one-shot sentinel `tracked/.git/.cleaned-up`. Planner picks the cleaner idempotency pattern.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Diagnostic source (REQUIRED)
- `.planning/debug/doyle-sessions-seal-tracked-psyches.md` — Full diagnosis + smoking-gun evidence + proposed-fix sketches for every candidate. Plans MUST treat this as the spec for "what the bug actually is."

### Roadmap entry
- `.planning/ROADMAP.md` §639 (Phase 25.2 section) — Fix candidate list, blast-radius bullets, cross-cutting integration mandate.

### Adjacent phase contexts (Phase 25.2 inherits decisions from these)
- `.planning/phases/24-tracked-dir-forked-repo-layout-agents-projects-branches-sess/24-CONTEXT.md` — Forked-repo layout, per-agent worktrees, sessions.log writer (D-13 "stop writing to flat `tracked/.git/`" is the root of #2).
- `.planning/phases/24.1-tracked-agents-info-json-cross-session-activity-index/24.1-CONTEXT.md` — Tracked-agent info.json + sessions-log commit funnel.
- `.planning/phases/25-perch-nesting-psyche-workers-wire-psyche-download-to-forked-/25-CONTEXT.md` — Nested perch topology (`owlery/<self>/nested/<self>-psyche/`); D-01..D-04 path-aware predicates; D-16 lazy `projects/{name}/` materialization. Plan 1's #4 sweep operates against this layout.

### Code anchors (REQUIRED — every plan must read its anchor before writing tasks)
- `src/common/tracked.rs:362-366` — `ensure_worktree` fast-path (Plan 1, #1 probe insertion site).
- `src/common/tracked.rs:1239-1269` — `seal_and_rotate_sessions_log` (Plan 1, #1 failing caller).
- `src/common/tracked.rs:1458` — `migrate_legacy_if_needed` (Plan 3, #2 cleanup site).
- `src/live/start.rs:138-178` — `drain_stale_signoff_file` (Plan 2, #5 rewrite site).
- `src/live/start.rs:583-630` — `emit_boot_trigger_after_spawn` (Plan 1, #3 retry budget).
- `src/live/signoff.rs:197-228` — signoff-side `read_wrapper_state` consumer (Plan 1, #3).
- `src/live/wrapper/mod.rs:629` — flat `ready_file()` call (Plan 1, #4 anchor — and the sentinel for the broader sweep).
- `src/live/wrapper/lifecycle.rs:116` — `drain_stale_init_signoffs` (Plan 2, #5 coexistence invariant).
- `src/live/wrapper/claude.rs:155-178` — wrapper post-`init_session` `write_wrapper_state` (Plan 1, #3 writer side).
- `src/common/wrapper_state.rs` — `wrapper_state_path` resolver + `read_wrapper_state` / `write_wrapper_state` (Plan 1, #3 + #4 path coherence).
- `src/common/owlery.rs:161` — `nested_perch_dir` helper (Plan 1, #4 target path constructor).
- `src/common/owlery.rs:351` — legacy `is_worker_perch` regex (already path-aware in Phase 25 D-03; cross-check Plan 1 #4).

### Resolved-debug docs to cross-check against (do NOT regress)
- `.planning/debug/resolved/init-signoff-substring-false-positive.md` — Plan 2 must preserve the envelope-shape match rule in `is_init_signoff_envelope`.
- `.planning/debug/resolved/stale-signoff-fires-on-next-session-start.md` — Plan 2's deliver-then-die must NOT reintroduce the STOP-loop the original drain prevented. Asymmetry argument is the invariant.
- `.planning/debug/resolved/phantom-init-signoff-after-handoff.md` — Plan 1's #3 retry budget interacts with the handoff-rehydration write path (`lifecycle.rs::new`). Confirm hydrated wrappers don't trip the elapsed-ms warning on the rehydrated read.
- `.planning/debug/resolved/wrapper-drains-stale-signoff.md` — historical precedent for the #5 fix; plan should reference for context.

### Skill docs (consult if Plan 2 envelope rendering changes user-visible output)
- `plugin/spt/skills/signoff/SKILL.md` — signoff user-facing contract.
- `plugin/spt/skills/commune/commune.md` — commune envelope shape reference.
- `plugin/spt/skills/live/SKILL.md` — echo_commune envelope catalog (§ "Two-slice body shape").

### Project-level (background)
- `.planning/PROJECT.md` — Reliable, zero-dependency agent-to-agent messaging contract.
- `CLAUDE.md` — Build/deploy + GSD workflow rules.

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **`commit_agent_payload` / `commit_project_payload`** (`src/common/tracked.rs`) — the funnel every per-agent-worktree git invocation routes through. Stale-lock probe (#1) inserted upstream in `ensure_worktree` lets one fix cover all callers.
- **`is_init_signoff_envelope`** (referenced by `src/live/wrapper/mod.rs` + `lifecycle.rs`) — predicate is envelope-shape-aware and is the SAME shape used by `drain_stale_init_signoffs`. `<EVENT type="latent signoff">` falls outside this predicate by construction, so #5's coexistence invariant is automatic.
- **`spool::spool_message`** + **TCP send path** (existing `$LIVE commune` infrastructure) — both exist. #5 just needs a small wrapper that constructs the EVENT envelope and calls the existing transport sequence (TCP first, spool fallback).
- **`wrapper_state_path` resolver** (`src/common/wrapper_state.rs`) — single source of truth for the `wrapper-state.json` path. Today resolves to flat top-level (`perch_dir(psyche_id)`). #4 likely needs this OR its callers to prefer nested when the psyche perch is nested. Audit step in Plan 1 task 1 confirms direction.
- **Phase 25 D-04 `enumerate_perches`** — already walks both flat and nested layers. Doctor / list tooling will pick up #4's swap transparently if the per-call-site swap is exhaustive.

### Established Patterns
- **Best-effort cleanup** (`fs::remove_file(...).map(|_| ()).unwrap_or(())` style with structured warning on Err) — used throughout `src/live/start.rs::relocate_legacy_psyche_if_needed` and `src/common/tracked.rs::migrate_legacy_if_needed`. Plan 1 #1 stale-lock probe follows the same idiom.
- **Phase 24 D-02 / Phase 32 D-05** — "best-effort per CONVENTIONS: failed migration emits one stderr warning and boot continues with legacy path." Same posture applies to #1 (stale-lock probe), #2 (ghost cleanup), #4 (flat→nested sweep — Plan should NOT hard-fail if a flat-path resolution still works as legacy fallback).
- **`read_wrapper_state` retry-on-empty loop** (`emit_boot_trigger_after_spawn`) — pattern is "poll path, sleep 250ms, retry; surface warning on exhaustion." #3 just dilates the budget. Pattern unchanged.
- **EVENT envelope convention** — Phase 23 D-04+ + Phase 25.1 echo_commune. Wrap body in `<EVENT type="...">...</EVENT>` with timestamp + source attrs. #5 reuses verbatim.

### Integration Points
- **Plan 1 ↔ Plan 2 file overlap** — both touch `src/live/start.rs`. Plan 1 touches `emit_boot_trigger_after_spawn` (#3 budget) + potentially `relocate_legacy_psyche_if_needed` regions (#4). Plan 2 touches `drain_stale_signoff_file`. Disjoint functions — no logical collision. If executed in parallel, merge-order is irrelevant.
- **Plan 1 ↔ Phase 25 partial-landing risk** — debug doc bonus finding flagged that some Phase 25 perch-nesting code is partially live (`nested_perch_dir` writer used by some psyche-wrapper instances; reader still flat). Plan 1's #4 sweep MUST resolve this asymmetry — either flip readers to nested (with flat fallback for legacy wrappers) OR force writers back to flat for the migration window. Recommendation captured in debug doc §Optional hardening: try nested first, fall back to flat. Planner picks the direction in Plan 1 task 2.
- **Plan 2 ↔ Phase 30 invariant** — `route_two_slice_signoff` writes `live_context.md` BEFORE attempting git commit. #5's "deliver then die" must preserve this ordering: surface body via TCP/spool BEFORE deleting `.claude/<id>-signoff.md`. Never delete before queue confirmed.
- **Plan 3 ↔ doctor invocation** — `migrate_legacy_if_needed` runs on every binary boot. Adding the ghost-repo cleanup there is idempotent + zero-cost when the ghost is already gone. Plan should add a single existence check + `remove_dir_all` + a "we cleaned the ghost" eprintln (once).

</code_context>

<specifics>
## Specific Ideas

- **Envelope name: `latent signoff`** (NOT "rescued signoff"). User-directed rename during discussion. Captures that the body sat dormant on disk until $LIVE start surfaced it.
- **Wrapper-state retry budget = 80 attempts × 250ms = 20s.** Specific numeric — matches cold-`claude -p` p99 latency. Planner can deviate up to ~120 attempts if there's strong evidence of a longer tail, but anything <60 is too tight.
- **#6 (haiku signoff-absorption) is OUT OF SCOPE.** Listed in ROADMAP §639 but explicitly dropped by user during discuss-phase. Do NOT touch psyche.md or commune-absorption prompts in this phase. The phase scope is **5 candidates: #1, #2, #3, #4, #5**.
- **Manual unblock for current doyle outage** (apply before Plan 1 lands if not already): `Remove-Item "$env:LOCALAPPDATA\spt\psyches\tracked\seed\worktrees\doyle\index.lock"`. Same sweep for any other agent showing seal warnings (deployah, dunsen, executor, higsby, mica, todlando, webber, witty).

</specifics>

<deferred>
## Deferred Ideas

- **#6 Haiku signoff-absorption compression** — User dropped from phase scope (was meant to be flagged as ignoreable, got rolled in by mistake). Not a structural defect; perceived compression was a Psyche-LLM content judgment. If revisited later: would be a `psyche.md` `<init_signoff>` + `<context_save>` prompt edit, optionally with a sessions.log raw-row backfill pass.
- **Migration coexistence policy refinement** — Phase 25 partial-landing means some wrappers write to `nested_perch_dir`, some to flat `perch_dir`. Plan 1's #4 sweep adopts a try-nested-first-fall-back-flat fallback chain (per debug-doc §Optional hardening). If this fallback policy needs to become a project-wide convention beyond Phase 25.2, capture in `.planning/CONVENTIONS.md` (out of scope here).
- **Cross-agent stale-lock sweep skill** — Plan 1's #1 stale-lock probe runs on the agent currently being booted. A separate `$OWL doctor --sweep-stale-locks` subcommand would scan ALL `seed/worktrees/*/index.lock` files at once. Useful operationally; not required by the phase. Capture as future enhancement.
- **`drain_stale_init_signoffs` deduplication** — Phase 25.2 #5 doesn't touch it, but the same predicate-based drain could be generalized to handle any deliver-then-die envelope (not just INIT_SIGNOFF). Out of scope; future hardening.

</deferred>

---

*Phase: 25.2-doyle-cluster-fix-candidates*
*Context gathered: 2026-05-22*
