# Phase 25.2: Doyle Cluster Fix Candidates - Pattern Map

**Mapped:** 2026-05-22
**Files analyzed:** 9 modified (production) + 3 test surfaces
**Analogs found:** 9 / 9 (every modification site has at least one in-tree analog)

This is a tactical-edit phase. Every fix candidate has a verified anchor (CONTEXT.md §canonical_refs + RESEARCH.md §"Code Anchors - Verified"). All patterns are in-tree; no greenfield architecture. Pattern excerpts below are grouped by plan (`25.2-01`, `25.2-02`, `25.2-03`) and per-candidate (#1..#5) for direct copy into plan task actions.

## File Classification

| Modified File | Role | Data Flow | Plan | Closest Analog | Match Quality |
|---------------|------|-----------|------|----------------|---------------|
| `src/common/tracked.rs` (`ensure_worktree` L335-433) | tracked-repo git-worktree resolver | stale-lock probe + best-effort fs::remove_file | 25.2-01 (#1) | `src/live/start.rs::relocate_legacy_psyche_if_needed` L196-229 (best-effort cleanup with structured warning) | exact (idiom + posture) |
| `src/common/tracked.rs` (`migrate_legacy_if_needed` L1458+) | tracked-repo migration scan | best-effort dir-removal | 25.2-03 (#2) | self-pattern at L1572-1614 (legacy rename + soft-fail) + `relocate_legacy_psyche_if_needed` | exact (same function, same soft-fail policy) |
| `src/common/wrapper_state.rs` (NEW: `WRAPPER_STATE_MAX_ATTEMPTS` const + `wrapper_state_path_resolved` + `read_wrapper_state_with_retry`) | shared wrapper-state surface | polled read with retry budget + nested-first-flat-fallback path resolver | 25.2-01 (#3+#4) | existing module structure (`wrapper_state_path` resolver L117-119; `read_wrapper_state` L155-159); retry loop pattern lifted from `start.rs::emit_boot_trigger_after_spawn` L590-630 | exact (extension of existing surface) |
| `src/live/start.rs::emit_boot_trigger_after_spawn` (L583-630) | listener-spawn sessions-log emitter | wrapper-state read + retry + warning | 25.2-01 (#3) | self-pattern at L590-630 (today's 8-attempt loop) - replaced by call to new shared helper | exact (collapse-to-call) |
| `src/live/start.rs::drain_stale_signoff_file` (L138-178) | listener-spawn latent-signoff drainer | rewrite: surface-then-delete -> deliver-then-die | 25.2-02 (#5) | `src/live/signoff.rs::run` L235-249 (compose payload + `catch_unwind` + `deliver_body_anonymous`); envelope construction from `src/owl/echo_commune.rs::compose_echo_commune_payload` L68-83 | exact (transport + envelope idioms) |
| `src/live/start.rs::run` post-relocate region | listener-spawn driver | path-aware perch-dir composition | 25.2-01 (#4) | self-pattern at L264 (`nested_perch_dir` already used) | exact |
| `src/live/signoff.rs::emit_signoff_trigger` (L196-231) | signoff-side sessions-log emitter | wrapper-state read with NO retry (today) | 25.2-01 (#3) | new shared helper (same as start.rs #3 callsite) | exact |
| `src/live/wrapper/mod.rs` flat-path call sites (L629, L700, L859, L1100) | wrapper poll-sentinel + handoff writer | psyche-perch ready-file probe + wrapper-state write | 25.2-01 (#4) | `ready_file_at` / `info_file_at` / `inbox_dir_at` path-aware helpers at `owlery.rs:177-191`; `nested_perch_dir` at `owlery.rs:161` | exact (path-aware helpers exist) |
| `src/live/wrapper/lifecycle.rs` (L23, L92) | wrapper-state handoff reader + cleanup | psyche-perch path resolution | 25.2-01 (#4) | same `nested_perch_dir` + `wrapper_state_path_resolved` | exact |
| `src/live/wrapper/echo_fire.rs` (L111) | wrapper echo-fire sentinel writer | psyche-perch `.more-done` marker | 25.2-01 (#4) | `nested_perch_dir(self_id, psyche_id).join(".more-done")` | exact |
| `src/live/wrapper/orphan.rs` (L242) | wrapper orphan-detect path | psyche-perch ready-file probe | 25.2-01 (#4) | same | exact |
| `src/live/wrapper/claude.rs` (L155-178) | wrapper post-init writer | publish wrapper-state.json | 25.2-01 (#4) | use new `wrapper_state_path_resolved` for write target | exact |
| **Tests** (module-local under each modified file) | unit tests | SPT_HOME sandbox + ENV_LOCK + SptHomeSnapshot | all 3 plans | `src/common/wrapper_state.rs::tests` L162-340 (canonical SptHomeSnapshot pattern); `src/common/tracked.rs::tests` L3110-3143 (`migrate_legacy_dot_git_left_in_place`); `src/live/wrapper/orphan.rs::orphan_fire_tests` L272-322 (write_test_info_json fixture helper); `src/live/wrapper/mod.rs::is_init_signoff_envelope_tests` L2752+ (predicate-regression test family - extend for D-12 invariant) | exact |

## Pattern Assignments

### Plan 25.2-01: Wrapper-Path-Correctness Sweep (#1 + #3 + #4)

---

#### `src/common/tracked.rs::ensure_worktree` (tracked-repo resolver, stale-lock probe insertion) - #1

**Anchor:** L335-433 (function); insertion site is L362-366 (fast-path return).

**Analog:** `src/live/start.rs::relocate_legacy_psyche_if_needed` L196-229 (best-effort cleanup with structured warning) - the canonical idiom for "soft-fail + one stderr warning + continue."

**Current fast-path** (L361-366, what we wrap with the probe):
```rust
// 4. Fast-path: worktree already materialized. The marker is `{wt}/.git`
//    as a FILE ...
let wt = scope.worktree_path(name);
let dotgit = wt.join(".git");
if dotgit.exists() {
    return Ok(wt);
}
```

**Best-effort cleanup pattern to copy** (lifted from `start.rs:208-227` and adapted to the probe):
```rust
// Phase 25.2 #1 - stale index.lock probe. Insert BEFORE `return Ok(wt)`.
// The lockfile lives at `seed/worktrees/{name}/index.lock` per git internals.
let lockfile = seed.join("worktrees").join(name).join("index.lock");
if let Ok(meta) = std::fs::metadata(&lockfile) {
    let stale = meta.len() == 0
        && meta.modified().ok()
            .and_then(|m| m.elapsed().ok())
            .map(|d| d.as_secs() > 60)
            .unwrap_or(false);
    if stale {
        match std::fs::remove_file(&lockfile) {
            Ok(_) => eprintln!(
                "tracked: removed stale index.lock at {} (>60s old, 0 bytes)",
                owlery::to_forward_slash(&lockfile)
            ),
            Err(e) => eprintln!(
                "WARNING: failed to remove stale index.lock at {}: {} (continuing)",
                owlery::to_forward_slash(&lockfile), e
            ),
        }
    }
}
```

**Why this analog:** Same module (tracked.rs already has soft-fail migration plumbing); same posture (warn + continue); reuses `owlery::to_forward_slash` for stable cross-platform log output. The mtime + size predicate is novel - git itself never writes a 0-byte index.lock during a healthy commit, so the conjunction (`len == 0 AND mtime > 60s ago`) catches the stuck-process case without false-positiving on a live `git commit` in flight.

**Tests for #1** - module-local under `src/common/tracked.rs::tests`. Pattern adapted from `wrapper_state.rs::tests` L226-275:
```rust
#[test]
fn ensure_worktree_removes_stale_index_lock() {
    let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let _snap = EnvSnapshot::capture();
    let tmp = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", tmp.path());

    // First ensure to materialize the worktree.
    ensure_agent_worktree("doyle").expect("first ensure must succeed");

    // Plant a stale 0-byte index.lock with mtime 5 minutes ago.
    let lockfile = ensure_seed().unwrap().join("worktrees").join("doyle").join("index.lock");
    std::fs::write(&lockfile, "").unwrap();
    // (apply 5-min-ago mtime via filetime crate OR raw set_modified)

    // Second ensure must remove the stale lock.
    ensure_agent_worktree("doyle").expect("second ensure must succeed");
    assert!(!lockfile.exists(), "stale index.lock must be removed");
}
```
Companion negative test: `ensure_worktree_preserves_fresh_index_lock` (same setup, current mtime, assert NOT removed). Wave 0 gap (RESEARCH §"Wave 0 Gaps"): confirm `filetime` dep present in `Cargo.toml`; if absent, raw `set_modified` syscall workaround.

---

#### `src/common/wrapper_state.rs` (NEW: shared retry helper + nested-first resolver) - #3 + #4

**Anchor:** existing file at L1-159 (complete). Append: `WRAPPER_STATE_MAX_ATTEMPTS` const, `wrapper_state_path_resolved(self_id, psyche_id)`, `read_wrapper_state_with_retry(self_id, psyche_id, site)`.

**Existing surface to extend** (L117-159):
```rust
pub fn wrapper_state_path(agent_id: &str) -> PathBuf {
    owlery::perch_dir(agent_id).join("wrapper-state.json")
}

pub fn write_wrapper_state(agent_id: &str, state: &WrapperHandoffState) -> std::io::Result<()> {
    let path = wrapper_state_path(agent_id);
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    write_atomic(&path, state)
}

pub fn read_wrapper_state(agent_id: &str) -> Option<WrapperHandoffState> {
    let path = wrapper_state_path(agent_id);
    let content = std::fs::read_to_string(&path).ok()?;
    serde_json::from_str::<WrapperHandoffState>(&content).ok()
}
```

**Retry-loop pattern to lift** (from `src/live/start.rs:590-630`, today's 8-attempt budget):
```rust
const MAX_ATTEMPTS: u32 = 8;
const RETRY_DELAY_MS: u64 = 250;
let mut attempt = 0;
loop {
    attempt += 1;
    match crate::common::wrapper_state::read_wrapper_state(psyche_id) {
        Some(state) if !state.session_uuid.is_empty() => {
            // success arm - emit row, return.
        }
        _ if attempt < MAX_ATTEMPTS => {
            std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS));
        }
        _ => {
            eprintln!(
                "WARNING: wrapper-state.json missing or empty session_uuid for {} after {}ms; skipping boot row",
                self_id, MAX_ATTEMPTS as u64 * RETRY_DELAY_MS,
            );
            return;
        }
    }
}
```

**New surface (extension to copy into `src/common/wrapper_state.rs`)** - matches D-05/D-06/D-07/D-08:
```rust
/// Phase 25.2 #3 - shared retry budget across boot/signoff/commune/pulse/init.
/// 80 attempts x 250ms = 20s ceiling. Matches cold `claude -p` p99 latency.
pub const WRAPPER_STATE_MAX_ATTEMPTS: u32 = 80;
pub const WRAPPER_STATE_RETRY_DELAY_MS: u64 = 250;

/// Phase 25.2 #4 - nested-first resolution with flat fallback.
/// Returns nested path when its file exists, else falls back to flat.
pub fn wrapper_state_path_resolved(self_id: &str, psyche_id: &str) -> PathBuf {
    let nested = owlery::nested_perch_dir(self_id, psyche_id).join("wrapper-state.json");
    if nested.exists() {
        return nested;
    }
    owlery::perch_dir(psyche_id).join("wrapper-state.json")
}

/// Polled read with shared budget. Returns None on exhaustion (caller soft-fails).
/// `site` is one of "boot" / "signoff" / "commune" / "pulse" / "init" for D-08 log.
pub fn read_wrapper_state_with_retry(self_id: &str, psyche_id: &str, site: &str)
    -> Option<WrapperHandoffState>
{
    let start = std::time::Instant::now();
    for attempt in 1..=WRAPPER_STATE_MAX_ATTEMPTS {
        let path = wrapper_state_path_resolved(self_id, psyche_id);
        if let Ok(content) = std::fs::read_to_string(&path) {
            if let Ok(state) = serde_json::from_str::<WrapperHandoffState>(&content) {
                if !state.session_uuid.is_empty() {
                    eprintln!(
                        "wrapper-state read: {}ms ({})",
                        start.elapsed().as_millis(), site
                    );
                    return Some(state);
                }
            }
        }
        if attempt < WRAPPER_STATE_MAX_ATTEMPTS {
            std::thread::sleep(std::time::Duration::from_millis(WRAPPER_STATE_RETRY_DELAY_MS));
        }
    }
    eprintln!(
        "WARNING: wrapper-state.json missing or empty session_uuid for {} after {}ms ({}); skipping row",
        self_id,
        WRAPPER_STATE_MAX_ATTEMPTS as u64 * WRAPPER_STATE_RETRY_DELAY_MS,
        site,
    );
    None
}
```

**Tests** (extend `src/common/wrapper_state.rs::tests` mod):
- `wrapper_state_path_resolved_prefers_nested_when_exists` - plant nested file, assert nested path returned.
- `wrapper_state_path_resolved_falls_back_to_flat_when_nested_missing` - plant flat file only, assert flat returned.
- `read_wrapper_state_with_retry_succeeds_after_delayed_write` - background thread writes after N ms; assert Some(state) within budget.
- `read_wrapper_state_with_retry_returns_none_after_budget` - never-written path; assert None + warning (use shortened test-only budget via cfg(test) or local const override - planner picks).

Test scaffolding lifted verbatim from `src/common/wrapper_state.rs:162-184` (`ENV_LOCK` + `SptHomeSnapshot`).

---

#### `src/live/start.rs::emit_boot_trigger_after_spawn` (L583-630) - #3 caller collapse

**Anchor:** L583-630 (function body).

**Today** (L590-630, what gets replaced):
```rust
const MAX_ATTEMPTS: u32 = 8;
const RETRY_DELAY_MS: u64 = 250;
let mut attempt = 0;
loop {
    attempt += 1;
    match crate::common::wrapper_state::read_wrapper_state(psyche_id) {
        Some(state) if !state.session_uuid.is_empty() => {
            if let Err(e) = crate::common::tracked::append_session_entry(
                self_id, &state.session_uuid, "boot",
            ) { ... }
            let _ = crate::common::owlery::bump_tracked_agent_info(...);
            return;
        }
        _ if attempt < MAX_ATTEMPTS => { std::thread::sleep(...); }
        _ => { eprintln!("WARNING: ..."); return; }
    }
}
```

**After** (collapse to single helper call, signature change adds `self_id` to thread through resolver):
```rust
match crate::common::wrapper_state::read_wrapper_state_with_retry(self_id, psyche_id, "boot") {
    Some(state) => {
        if let Err(e) = crate::common::tracked::append_session_entry(
            self_id, &state.session_uuid, "boot",
        ) {
            eprintln!(
                "WARNING: sessions log boot append failed for {}: {} (continuing)",
                self_id, e
            );
        }
        let _ = crate::common::owlery::bump_tracked_agent_info(self_id, "boot", &[], "");
    }
    None => { /* helper already emitted WARNING - return silently */ }
}
```

**Why this collapse:** D-06 mandates "drift impossible" - single shared const, single retry loop. The two consts at L592-593 are exactly the divergence D-06 closes.

---

#### `src/live/signoff.rs::emit_signoff_trigger` (L196-231) - #3 caller upgrade

**Anchor:** L196-231 (function body).

**Today** (no retry at all - single shot read at L197):
```rust
fn emit_signoff_trigger(self_id: &str, psyche_id: &str) {
    match crate::common::wrapper_state::read_wrapper_state(psyche_id) {
        Some(state) if !state.session_uuid.is_empty() => {
            if let Err(e) = crate::common::tracked::append_session_entry(
                self_id, &state.session_uuid, "signoff",
            ) { ... }
            // ... bump_tracked_agent_info on success
        }
        _ => {
            eprintln!(
                "WARNING: wrapper-state.json missing or empty session_uuid for signoff of {}; sessions.log row skipped",
                self_id
            );
        }
    }
}
```

**After** (route through shared helper - same idiom as #3 boot collapse):
```rust
fn emit_signoff_trigger(self_id: &str, psyche_id: &str) {
    match crate::common::wrapper_state::read_wrapper_state_with_retry(self_id, psyche_id, "signoff") {
        Some(state) => {
            if let Err(e) = crate::common::tracked::append_session_entry(
                self_id, &state.session_uuid, "signoff",
            ) {
                eprintln!(
                    "WARNING: sessions log signoff append failed for {}: {} (continuing)",
                    self_id, e
                );
            }
            let names = crate::common::owlery::derive_current_repo_names();
            let branch = crate::common::git::head_branch_or_empty(
                &std::env::current_dir().unwrap_or_default(),
            );
            let _ = crate::common::owlery::bump_tracked_agent_info(
                self_id, "signoff", &names, &branch,
            );
        }
        None => { /* helper already emitted WARNING */ }
    }
}
```

**Other #3 consumers to upgrade identically** (per D-06): commune trigger, pulse trigger, init trigger - find via `Grep` for `read_wrapper_state(` callers; replace each with the `_with_retry` variant using the appropriate `site` literal.

---

#### `src/live/wrapper/mod.rs:629, 700, 859, 1100` + sibling sites - #4 flat->nested sweep

**Anchor matrix:** RESEARCH §"#4 Call-Site Enumeration" (13 sites total; 8 need nested swap, 3 stay flat (Self side), 2 test-only).

**Path-aware helper API to use** (already exists at `src/common/owlery.rs:161, 177-191`):
```rust
/// Phase 25 D-01: returns the nested perch directory for a child under its parent:
/// `owlery/<parent>/nested/<child_id>/`. Pure path composition.
pub fn nested_perch_dir(parent: &str, child_id: &str) -> PathBuf {
    owlery_dir().join(parent).join("nested").join(child_id)
}

/// Phase 25 B1: returns the ready sentinel under a known perch path:
/// `<perch_path>/ready`. Pure path composition.
pub fn ready_file_at(perch_path: &Path) -> PathBuf {
    perch_path.join("ready")
}
pub fn info_file_at(perch_path: &Path) -> PathBuf { perch_path.join("info.json") }
pub fn inbox_dir_at(perch_path: &Path) -> PathBuf { perch_path.join("inbox") }
```

**Mechanical swap pattern** - every psyche-side site changes from:
```rust
// BEFORE (flat):
let ready_exists = owlery::ready_file(&self.psyche_id).exists();
```
to:
```rust
// AFTER (nested):
let ready_exists = owlery::ready_file_at(
    &owlery::nested_perch_dir(&self.self_id, &self.psyche_id)
).exists();
```

**For wrapper-state path sites (`lifecycle.rs:23`, `mod.rs:1100`)** - route through new resolver instead of inline-joining:
```rust
// BEFORE (inline flat):
let path = owlery::perch_dir(&psyche_id).join("wrapper-state.json");

// AFTER (resolver - nested-first-flat-fallback):
let path = crate::common::wrapper_state::wrapper_state_path_resolved(&self_id, &psyche_id);
```

**Sites that STAY FLAT** (Self side, per Phase 25 D-01 "Self perch dir stays flat - it IS the parent"):
- `src/live/wrapper/echo_fire.rs:152` - `owlery::info_file(&self.self_id)`
- `src/live/wrapper/orphan.rs:59, 142` - Self info_file / Self perch_dir
- Test helpers at `orphan.rs:309, 321` - planner discretion (TEST-ONLY)

**Why reader-side, not writer-side** (RESEARCH §"#4 Migration Coexistence Direction"): Phase 25 partial-landing means writers are mixed (gen-old=flat, gen-new=nested). Reader-side try-nested-first-fall-back-flat heals automatically. Writer flip would break in-flight Phase 18.4/18.5 binary handoffs.

**Anti-pattern (DO NOT)**: do NOT remove `owlery::ready_file(id)` / `owlery::perch_dir(id)` themselves - they remain correct for Self-side call sites. The swap is per-site, not whole-API.

---

### Plan 25.2-02: Latent-Signoff Forward (#5)

---

#### `src/live/start.rs::drain_stale_signoff_file` (L138-178) - REWRITE

**Anchor:** L138-178 (current function: read body, surface to stdout, delete).

**Today** (what we replace):
```rust
pub(crate) fn drain_stale_signoff_file(id: &str, cwd: &Path) {
    let signoff_path = cwd.join(".claude").join(format!("{}-signoff.md", id));
    if !signoff_path.exists() { return; }
    match fs::read_to_string(&signoff_path) {
        Ok(body) => {
            let trimmed = body.trim_end();
            if !trimmed.is_empty() {
                output::live_status(...);
                println!(
                    "<owl_pending_signoff id=\"{}\" cleared_from=\"{}\">\n{}\n</owl_pending_signoff>",
                    id, owlery::to_forward_slash(&signoff_path), trimmed,
                );
            }
            if let Err(e) = fs::remove_file(&signoff_path) { ... }
        }
        Err(e) => { ... }
    }
}
```

**Analog 1 (transport + catch_unwind)** - `src/live/signoff.rs:241-249`:
```rust
if owlery::ready_file(&psyche_id).exists() {
    // Send INIT_SIGNOFF, optionally with bundled final commune
    let timestamp = crate::common::time::format_timestamp();
    let stamp = crate::common::git::stamp();
    let stash_final = compose_init_signoff_payload(id, &timestamp, &stamp, message);
    let _ = std::panic::catch_unwind(|| {
        send::deliver_body_anonymous(&psyche_id, &stash_final);
    });
    ...
}
```

**Analog 2 (EVENT envelope construction)** - `src/owl/echo_commune.rs:68-83` (canonical typed-envelope `format!` shape):
```rust
pub(crate) fn compose_echo_commune_payload(
    from: &str, timestamp: &str, stamp: &crate::common::git::Stamp,
    note: &str, body: &str,
) -> String {
    format!(
        "<EVENT type=\"echo_commune\" from=\"{}\" timestamp=\"{}\"{} note=\"{}\">{}</EVENT>",
        crate::owl::poll::event_attr_escape(from),
        crate::owl::poll::event_attr_escape(timestamp),
        stamp.event_attrs(),
        crate::owl::poll::event_attr_escape(note),
        crate::owl::poll::event_body_escape(body),
    )
}
```

**Analog 3 (escape helpers)** - `src/owl/poll.rs:685-705`:
```rust
pub(crate) fn event_body_escape(s: &str) -> String {
    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
        .replace('"', "&quot;").replace('\n', "<br>")  // \n -> <br> LAST
}
pub(crate) fn event_attr_escape(s: &str) -> String {
    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
}
```

**Analog 4 (transport surface)** - `src/owl/send.rs:65-97, 268-276`:
```rust
fn deliver_message(target: &str, from: &str, body: &str) {
    let owlery = owlery::owlery_dir();
    if let Some(addr_str) = registry::lookup_address(target, &owlery) {
        if let Ok(addr) = addr_str.parse::<std::net::SocketAddr>() {
            let timeout = Duration::from_secs(2);
            match TcpStream::connect_timeout(&addr, timeout) {
                Ok(mut stream) => {
                    if protocol::write_message(&mut stream, from, body).is_ok() {
                        return; // TCP succeeded
                    }
                }
                Err(_) => {
                    if let Some(pid) = registry::lookup_pid(target, &owlery) {
                        if !types::is_process_alive(pid) {
                            let _ = registry::unregister_address(target, &owlery);
                        }
                    }
                }
            }
        }
    }
    let _ = spool::spool_message(target, from, body, &owlery); // Spool fallback
}

pub fn deliver_body_anonymous(target: &str, body: &str) {
    deliver_message(target, "", body);
}
```

**Rewrite (deliver-then-die, D-09 + D-10 + D-11)** - inline envelope construction per RESEARCH "Don't Hand-Roll" rationale (one call site; helper overhead not warranted):
```rust
pub(crate) fn drain_stale_signoff_file(id: &str, cwd: &Path) {
    let signoff_path = cwd.join(".claude").join(format!("{}-signoff.md", id));
    if !signoff_path.exists() { return; }

    let body = match fs::read_to_string(&signoff_path) {
        Ok(b) => b,
        Err(e) => {
            output::owl_err(&format!(
                "drain_stale_signoff_file: failed to read {}: {} (continuing $LIVE start)",
                owlery::to_forward_slash(&signoff_path), e,
            ));
            return;
        }
    };
    let trimmed = body.trim_end();
    if trimmed.is_empty() {
        // Empty file - just delete and return; nothing to forward.
        let _ = fs::remove_file(&signoff_path);
        return;
    }

    // D-11: build the <EVENT type="latent signoff"> envelope inline.
    // Body MUST start at byte 0 with `<EVENT type="latent signoff"` per
    // poll.rs body_is_typed_event_envelope passthrough rule (RESEARCH Pitfall 2).
    let psyche_id = format!("{}-psyche", id);
    let written_at = match fs::metadata(&signoff_path)
        .and_then(|m| m.modified())
    {
        Ok(t) => {
            let dt: chrono::DateTime<chrono::Local> = t.into();
            dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string()
        }
        Err(_) => String::new(),
    };
    let envelope = format!(
        "<EVENT type=\"latent signoff\" from=\"{}\" written_at=\"{}\" cleared_from=\"{}\">{}</EVENT>",
        crate::owl::poll::event_attr_escape(id),
        crate::owl::poll::event_attr_escape(&written_at),
        crate::owl::poll::event_attr_escape(&owlery::to_forward_slash(&signoff_path)),
        crate::owl::poll::event_body_escape(trimmed),
    );

    // D-10: TCP-first, spool-fallback via deliver_body_anonymous.
    // catch_unwind mirrors src/live/signoff.rs:247-249.
    let psyche_id_for_send = psyche_id.clone();
    let envelope_for_send = envelope.clone();
    let delivered = std::panic::catch_unwind(move || {
        send::deliver_body_anonymous(&psyche_id_for_send, &envelope_for_send);
    }).is_ok();

    output::live_status(
        output::S_READY,
        &format!(
            "LATENT-SIGNOFF-FORWARDED:{} (body queued to {} via TCP/spool)",
            id, psyche_id
        ),
    );

    // D-10: deliver-then-die. Delete only AFTER queue confirmed.
    if delivered {
        if let Err(e) = fs::remove_file(&signoff_path) {
            output::owl_err(&format!(
                "drain_stale_signoff_file: failed to remove {}: {} (body was forwarded; next listener iteration will retry)",
                owlery::to_forward_slash(&signoff_path), e,
            ));
        }
    } else {
        output::owl_err(&format!(
            "drain_stale_signoff_file: forward panicked for {}; preserving signoff file for next attempt",
            owlery::to_forward_slash(&signoff_path),
        ));
    }
}
```

**Note** - the `delivered` boolean is currently coarse (only catches a panic). Per RESEARCH §"Wave 0 Gaps" + Open Question #1, the planner decides whether to:
(a) accept the weaker form (only panic preserves the file - transport-level NO_PERCH still spools and we trust the spool), OR
(b) refactor `deliver_body_anonymous` to return a success/queue-status so the file is preserved only on hard failure.

CONTEXT D-10 says "deliver-then-die - never delete before queue confirmed." Since `deliver_body_anonymous` ALWAYS reaches the spool fallback (even when TCP fails), in practice (a) preserves the contract: spool write is a durable destination. (b) is gold-plating - defer.

**Coexistence invariant (D-12)** - the substring `<EVENT type="latent signoff"` does NOT match `is_init_signoff_envelope` at `src/live/wrapper/mod.rs:148-151`:
```rust
pub(crate) fn is_init_signoff_envelope(msg: &str) -> bool {
    msg.to_ascii_lowercase()
        .contains("<event type=\"init_signoff\"")
}
```
The wrapper-side `drain_stale_init_signoffs` (`lifecycle.rs:132-136`) uses this predicate exclusively, so latent-signoff envelopes are correctly ignored by it.

**Tests for #5** - module-local under `src/live/start.rs`:

1. `drain_stale_signoff_file_forwards_latent_signoff_and_deletes_file` - plant signoff file in tempdir cwd + plant psyche perch under SPT_HOME; call drain; assert file deleted AND spool row contains `<EVENT type="latent signoff"` envelope.

2. Extend `src/live/wrapper/mod.rs::is_init_signoff_envelope_tests` (L2752+) with `latent_signoff_envelope_is_not_init_signoff_envelope`:
```rust
#[test]
fn latent_signoff_envelope_is_not_init_signoff_envelope() {
    let payload = r#"<EVENT type="latent signoff" from="doyle" written_at="2026-05-22T12:34:56-07:00" cleared_from="/foo/.claude/doyle-signoff.md">prior session brief body</EVENT>"#;
    assert!(
        !is_init_signoff_envelope(payload),
        "latent signoff envelope must NOT match is_init_signoff_envelope (D-12 invariant)"
    );
}
```

Test scaffolding pattern (SPT_HOME + ENV_LOCK + SptHomeSnapshot) lifted from `src/common/wrapper_state.rs:162-184`.

---

### Plan 25.2-03: Ghost `tracked/.git/` Cleanup (#2)

---

#### `src/common/tracked.rs::migrate_legacy_if_needed` (L1458+) - ghost cleanup insertion

**Anchor:** L1458 (function start) + RAII re-entry guard L1464-1475 + scan loop L1487+.

**Analog 1 (existing soft-fail posture in same function)** - L1538+ (per-agent migration with single stderr warning + continue):
```rust
let mut migrated_count: usize = 0;
for agent_id in &agent_ids {
    // ... per-agent migration; failures emit stderr and skip the agent.
}
```

**Analog 2 (best-effort remove + structured warning)** - the same `relocate_legacy_psyche_if_needed` idiom from L196-229 of start.rs.

**Insertion site** - near the top of `migrate_legacy_if_needed`, AFTER the re-entry guard fires (L1475), BEFORE the agent-ids scan begins (L1485). Unconditional + idempotent + soft-fail:
```rust
// Phase 25.2 #2 - ghost `tracked/.git/` cleanup. Phase 23-era dormant repo;
// Phase 24 D-13 stopped writing to it. Cosmetic noise only - no live reader.
// Unconditional best-effort `remove_dir_all`: when the dir is gone the
// `exists()` check short-circuits to a single stat-syscall (no sentinel needed).
let ghost = owlery::tracked_root().join(".git");
if ghost.exists() {
    match std::fs::remove_dir_all(&ghost) {
        Ok(_) => eprintln!(
            "tracked: removed Phase 23-era ghost .git/ at {}",
            owlery::to_forward_slash(&ghost)
        ),
        Err(e) => eprintln!(
            "WARNING: failed to remove ghost tracked/.git/ at {}: {} (continuing)",
            owlery::to_forward_slash(&ghost), e
        ),
    }
}
```

**Why unconditional, not sentinel-guarded** (RESEARCH §"#2 Idempotency Pattern"):
1. Sentinel adds complexity for no gain - `Path::exists` is the same cost as reading a sentinel.
2. Sentinel introduces partial-state hazard - if sentinel is written but `remove_dir_all` partially fails (Windows file-locking), future invocations skip and the leak persists.
3. Symmetric to existing soft-fail posture in `migrate_legacy_if_needed`.
4. Easier test inversion - one test flips (`migrate_legacy_dot_git_left_in_place` -> `migrate_legacy_removes_ghost_dot_git`).

**Test inversion** - existing test at `src/common/tracked.rs:3110-3143`:
```rust
#[test]
fn migrate_legacy_dot_git_left_in_place() {
    // Pitfall 6: a stale Phase 23-era `.git/` directory at
    // `psyches/tracked/.git/` must NOT be cleaned up by migration.
    // ... plants .git/ + legacy file, runs ensure_seed, asserts .git/ remains.
    assert!(root.join(".git").is_dir(), "Phase 23-era .git/ must remain untouched (Pitfall 6)");
}
```

**Inverted** (Plan 3 task): rename to `migrate_legacy_removes_ghost_dot_git` and flip assertions:
```rust
#[test]
fn migrate_legacy_removes_ghost_dot_git() {
    // Phase 25.2 #2: a stale Phase 23-era `.git/` directory at
    // `psyches/tracked/.git/` IS now cleaned up by migration (inverts Pitfall 6).
    // ... plants .git/ + legacy file, runs ensure_seed.
    assert!(!root.join(".git").exists(), "Phase 23-era .git/ must be removed");
    // Sanity: per-agent migration still ran.
    assert!(owlery::agent_worktree_path("doyle").join("daemon.log").exists());
}
```

Idempotency companion:
```rust
#[test]
fn migrate_legacy_removes_ghost_dot_git_is_idempotent() {
    // Second invocation after cleanup must be a no-op (no panic, no warning).
    // ... run ensure_seed twice; assert second invocation succeeds.
}
```

---

## Shared Patterns

### Pattern S-1: Best-effort cleanup with structured warning

**Source:** `src/live/start.rs::relocate_legacy_psyche_if_needed` L196-229 (canonical instance); also `src/common/tracked.rs::migrate_legacy_if_needed` body (soft-fail per agent).

**Apply to:**
- Plan 1 #1 stale-lock probe (`ensure_worktree` insertion).
- Plan 3 #2 ghost cleanup (`migrate_legacy_if_needed` insertion).
- Plan 2 #5 file-removal arm (post-deliver).

**Canonical shape:**
```rust
match std::fs::<op>(&path) {
    Ok(_) => eprintln!("<subsystem>: <action> succeeded at {}", owlery::to_forward_slash(&path)),
    Err(e) => eprintln!("WARNING: <action> failed at {}: {} (continuing)", owlery::to_forward_slash(&path), e),
}
```

**Invariants:** never panic; never `?`-propagate to caller; always use `owlery::to_forward_slash` for cross-platform stable log output; always end the WARNING with "(continuing)" so log readers know the failure is non-fatal.

---

### Pattern S-2: SPT_HOME tempdir sandbox + ENV_LOCK + SptHomeSnapshot

**Source:** `src/common/wrapper_state.rs:162-184` (canonical instance, copied across 4+ modules already).

**Apply to:** every test in Plans 1, 2, 3 that touches `SPT_HOME`-resolved paths (every test in this phase qualifies).

**Canonical scaffold:**
```rust
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());

struct SptHomeSnapshot(Option<String>);
impl SptHomeSnapshot {
    fn capture() -> Self { Self(std::env::var("SPT_HOME").ok()) }
}
impl Drop for SptHomeSnapshot {
    fn drop(&mut self) {
        match &self.0 {
            Some(v) => std::env::set_var("SPT_HOME", v),
            None => std::env::remove_var("SPT_HOME"),
        }
    }
}

#[test]
fn my_test() {
    let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let _snap = SptHomeSnapshot::capture();
    let tmp = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", tmp.path());
    // ... test body uses owlery::* helpers which resolve under SPT_HOME.
}
```

**Run command:** `cargo test --release -- --test-threads=1` (CLAUDE.md mandate; serializes env-mutating tests across modules).

**Where the scaffold already exists in-tree** (reuse, don't redefine when test lives in the same module):
- `src/common/wrapper_state.rs:162-184` - canonical
- `src/common/owlery.rs::tests::ENV_LOCK` (referenced from wrapper_state.rs L167)
- `src/live/wrapper/orphan.rs:280-304` - `EnvSnapshot` variant (same idea, different name)
- `src/common/tracked.rs::tests` (used by `migrate_legacy_dot_git_left_in_place`)

**When adding tests to an existing test mod that already defines `ENV_LOCK` + a snapshot type, REUSE - do NOT redefine.**

---

### Pattern S-3: EVENT envelope construction (typed wire format)

**Source:** `src/owl/echo_commune.rs::compose_echo_commune_payload` L68-83 (canonical `format!` instance); helpers at `src/owl/poll.rs:685-705`.

**Apply to:** Plan 2 #5 latent-signoff envelope (inline at single call site - no helper).

**Canonical shape:**
```rust
let envelope = format!(
    "<EVENT type=\"<verb>\" from=\"{}\" timestamp=\"{}\" <other-attrs>=\"{}\">{}</EVENT>",
    crate::owl::poll::event_attr_escape(from),     // attrs: escape & < > "
    crate::owl::poll::event_attr_escape(timestamp),
    crate::owl::poll::event_attr_escape(other_attr_value),
    crate::owl::poll::event_body_escape(body),     // body: escape + \n -> <br>
);
```

**Invariants** (from `poll.rs:682` doc-comment + Pitfall 2):
- Envelope literal MUST start at byte 0 with `<EVENT type=` (no leading whitespace, no BOM) so `body_is_typed_event_envelope` short-circuits the outer re-wrap.
- Escape order: `&` first, then `<` `>` `"`, then (body only) `\n -> <br>` LAST.
- Attribute values never contain newlines (line-safe by construction).

---

### Pattern S-4: TCP-first transport with spool fallback + catch_unwind

**Source:** `src/owl/send.rs::deliver_message` L65-97 (transport); `src/owl/send.rs::deliver_body_anonymous` L272-276 (public anonymous wrapper); `src/live/signoff.rs:247-249` (catch_unwind shape).

**Apply to:** Plan 2 #5 latent-signoff delivery.

**Canonical caller shape (DO NOT hand-roll TCP - call the existing function):**
```rust
let _ = std::panic::catch_unwind(move || {
    send::deliver_body_anonymous(&psyche_id, &envelope);
});
```

**Why catch_unwind:** the transport may panic on registry corruption or rare encoding edges; the caller is on a path where panic during cleanup would lose user data. Existing `signoff.rs:247-249` precedent.

**`deliver_body_anonymous` already handles** (do not reimplement):
- registry lookup of target address
- 2-second TCP connect timeout
- protocol::write_message
- on TCP failure: stale-PID detection + registry cleanup
- spool fallback as durable last resort

---

## No Analog Found

None. Every modification site in this phase has a verified in-tree analog (see RESEARCH §"Code Anchors - Verified" and §"Architecture Patterns - Patterns to Reuse"). This is a pure tactical-edit phase against frozen anchors.

## Metadata

**Analog search scope:**
- `src/common/tracked.rs` (L335-433, L1239-1269, L1458-1535, L1572-1614, L3090-3160)
- `src/common/wrapper_state.rs` (entire file)
- `src/common/owlery.rs` (L140-220)
- `src/live/start.rs` (L130-250, L260-370, L580-630)
- `src/live/signoff.rs` (L190-260)
- `src/live/wrapper/mod.rs` (L140-152, L620-710, L2740-2790)
- `src/live/wrapper/lifecycle.rs` (L100-150)
- `src/live/wrapper/orphan.rs` (L230-322)
- `src/owl/send.rs` (L60-280)
- `src/owl/echo_commune.rs` (L60-90)
- `src/owl/poll.rs` (L680-710)

**Files scanned:** 11 production-code files, 4 module-local test families.
**Pattern extraction date:** 2026-05-22.
**Phase:** 25.2-doyle-cluster-fix-candidates-blast-radius-sanity-check-acros.
