---
phase: 25.4-perch-path-single-source-of-truth-finish-d-01-nested-migrati
plan: 06
subsystem: runtime
tags: [rust, perch-path, resolver, reader-migration, nested-migration, class-e, single-source-of-truth]

requires:
  - phase: 25.4
    provides: "Plan 01 perch_path resolver module + 25.4-03/04/05 writer-side nested-canonical migration"
provides:
  - "All eight RESEARCH Class-E reader sites route through perch_path::resolve_* with appropriate ParentHint"
  - "live/stop.rs::kill_psyche_wrapper symmetric with wrapper/lifecycle.rs:127 nested cleanup per CONTEXT D-09"
  - "touch_loop.rs scan_live_psyches() returns Vec<(String, PathBuf)> — perch_path bubbled up from enumerate_perches (RESEARCH §E-3)"
  - "Test fixtures in commune.rs / signoff.rs / cleanup.rs migrated to resolver-routed psyche paths so they exercise the same nested layout as production readers"
affects: [25.4-07]

tech-stack:
  added: []
  patterns:
    - "Reader migration: ParentHint::Explicit at sites where Self context is in scope (commune, signoff, stop::kill_psyche_wrapper); ParentHint::Infer at CLI-target sites (send, ring, boot_spine PSYCHE_DEAD, cleanup_perch helper)"
    - "Approach A (bubble-up perch_path from enumerate_perches) preferred over Approach B (resolver re-resolve in loop) when the enumerate_perches caller is the only consumer of the scan helper — matches the Phase 25 D-04 idiom"
    - "Test-fixture migration alongside production reader migration: when a reader's path composition changes, its tests' write-side fixtures must change in lockstep so the fixtures still target the path the reader reads"

key-files:
  created: []
  modified:
    - src/owl/send.rs
    - src/owl/ring.rs
    - src/owl/cleanup.rs
    - src/live/commune.rs
    - src/live/signoff.rs
    - src/live/touch_loop.rs
    - src/live/boot_spine.rs
    - src/live/stop.rs

key-decisions:
  - "All migrated files import via `use crate::common::perch_path::{self, ParentHint};` (two-line import pattern) rather than fully-qualified call-site paths. Mirrors the established import idiom from Plan 25.4-03 poll.rs migration."
  - "ParentHint::Explicit(self_id) chosen at commune.rs:60/120 and signoff.rs:174/253 — Self context is in scope (function arg) so explicit is unambiguous per CONTEXT D-04."
  - "ParentHint::Infer chosen at send.rs/ring.rs (all sites) — these are CLI-target surfaces where only the target id is available; suffix-strip correctly derives parent for psyche/worker."
  - "ParentHint::Infer chosen at boot_spine.rs:221 — Spine receives raw `PSYCHE_DEAD: <psyche_id>` body via Touch and has no Self context; suffix-strip derives the parent."
  - "ParentHint::Explicit(self_id) chosen at stop.rs:18 (kill_psyche_wrapper) — resolves CONTEXT D-09 asymmetry; explicit parent yields nested path symmetric with wrapper/lifecycle.rs:127."
  - "touch_loop.rs took Approach A (signature change to Vec<(String, PathBuf)>) over Approach B (in-loop resolver call). Approach A is the established Phase 25 D-04 idiom and scan_live_psyches has only one caller, so signature ripple is minimal."
  - "Test fixtures in commune.rs (commune_result_bumps_cursor_on_offline_spool), signoff.rs (signoff_result_bumps_cursor_when_psyche_reachable), and cleanup.rs (all four cleanup-test scenarios) migrated to resolver-routed writes so the fixtures match what the production readers now look up. Without this, the tests would silently break because the writes land flat while the reads now look nested."
  - "cleanup.rs production teardown code unchanged: the walker (`enumerate_perches` Layer 1 + Layer 2 inline-bumped walk) already produces correct per-perch paths; only the id-derived test helper paths needed migration per RESEARCH §E-8 (benign today but fragile)."
  - "stop.rs run_single / run_all NOT migrated: per RESEARCH §1 audit + actual code inspection, only line 18 was Class-E. run_single/run_all dispatch to kill_psyche_wrapper and crate::owl::stop::run; they do not compose perch paths directly."
  - "boot_spine.rs line 230 (owlery::ready_file(self_id)) left UNCHANGED per plan — self_id is Self-kind which Infer resolves flat byte-for-byte; resolver routing would produce the same path. Minimizes churn at a site that doesn't need it."

patterns-established:
  - "Pattern: every reader/writer of psyche/worker perch paths now routes through perch_path::resolve_* — direct owlery::perch_dir(id) for non-Self ids has been eliminated from src/owl/{send,ring,cleanup}.rs and src/live/{commune,signoff,touch_loop,boot_spine,stop}.rs."
  - "Pattern: test fixtures migrate in lockstep with their production readers — flat-write + nested-read silently breaks. The Edit must keep them symmetric."

requirements-completed: [PERCH-PATH-READER-MIGRATION-01, PERCH-PATH-READER-MIGRATION-02, PERCH-PATH-STOP-SYMMETRY-01]

duration: ~12min
completed: 2026-05-23
---

# Phase 25.4 Plan 06: Class-E Reader Migration + stop.rs D-09 Symmetry Summary

**Migrated all eight RESEARCH Class-E reader sites onto the central `perch_path` resolver and resolved the `live/stop.rs:18` ↔ `wrapper/lifecycle.rs:127` cleanup asymmetry per CONTEXT D-09. Test fixtures co-migrated so reads + writes agree on the same nested layout. Wave-3 reader half of Phase 25.4 now lands: every Class-E reader sees the same perch path the Wave-2 writers wrote to.**

## Performance

- **Duration:** ~12 min
- **Tasks:** 5 (one commit each, atomic per file group)
- **Files modified:** 8
- **Files created:** 0

## Accomplishments

- **Task 1** (send.rs + ring.rs): 18 call sites across `is_alive`, `check_alive`, `is_perch_exists`, `run`, `run_reply`, `send_result`, `ring::run`, `ring_result`, `cleanup_ephemeral` migrated to `perch_path::resolve_*` with `ParentHint::Infer`. Self ids return flat unchanged; psyche/worker ids resolve to the nested layout post-Wave-2.
- **Task 2** (cleanup.rs): `write_perch` test helper + four read-side assertions migrated to the resolver with `ParentHint::Infer`. Production teardown loop unchanged — the walker's per-perch `&path` was already correct. Eliminates RESEARCH §E-8's "benign today but fragile" classification.
- **Task 3** (commune.rs + signoff.rs): four psyche-readiness predicates (commune.rs L60/L120, signoff.rs L174/L253) migrated to `resolve_ready_file(&psyche_id, ParentHint::Explicit(self_id))`. Test fixtures `commune_result_bumps_cursor_on_offline_spool` and `signoff_result_bumps_cursor_when_psyche_reachable` migrated to resolver-routed writes for the psyche perch dir + info.json + ready file. Without the test-fixture migration, the post-write nested reads would not find the flat-written test fixtures and the tests would silently break.
- **Task 4** (touch_loop.rs + boot_spine.rs): `scan_live_psyches()` return type changed to `Vec<(String, PathBuf)>` so the touch loop consumes the perch_path bubbled up from `enumerate_perches` (Approach A per RESEARCH §E-3). `boot_spine.rs::handle_spine_message` PSYCHE_DEAD revival re-check and `cleanup_perch` local fn routed through `resolve_perch_path` with `ParentHint::Infer`. `boot_spine.rs:230` (`owlery::ready_file(self_id)`) left unchanged per plan — Self-kind is flat byte-for-byte under both APIs.
- **Task 5** (stop.rs): `kill_psyche_wrapper(self_id)` line 18 migrated to `resolve_perch_path(&psid, ParentHint::Explicit(self_id))`. Resolves the long-standing D-09 asymmetry where stop.rs was cleaning flat while wrapper/lifecycle.rs:127 was cleaning nested — both now symmetric on nested. Plan-speculated additional sites in run_single/run_all do not exist (those functions dispatch but don't compose paths).

## Task Commits

1. **Task 1: Migrate send.rs + ring.rs reader call sites** — `b16fdd9` (refactor)
2. **Task 2: Migrate cleanup.rs id-derived perch paths** — `e493348` (refactor)
3. **Task 3: Migrate commune.rs + signoff.rs psyche reads** — `8f3dbae` (refactor)
4. **Task 4: Migrate touch_loop + boot_spine PSYCHE_DEAD reads** — `acb9aa6` (refactor)
5. **Task 5: Migrate stop.rs::kill_psyche_wrapper (D-09 symmetry)** — `355cd5f` (refactor)

## Files Created/Modified

- `src/owl/send.rs` (modified) — 9 resolver calls; perch_path + ParentHint imports added.
- `src/owl/ring.rs` (modified) — 13 resolver calls covering target + ephemeral + cleanup paths.
- `src/owl/cleanup.rs` (modified) — 9 resolver calls in test helper + assertions; production teardown unchanged.
- `src/live/commune.rs` (modified) — 4 resolver calls (2 prod psyche-ready reads + 2 test fixture migrations).
- `src/live/signoff.rs` (modified) — 5 resolver calls (2 prod psyche-ready reads + 3 test fixture migrations).
- `src/live/touch_loop.rs` (modified) — `scan_live_psyches` signature change + bubble-up consumer; PathBuf import added.
- `src/live/boot_spine.rs` (modified) — 2 resolver calls (PSYCHE_DEAD revival re-check + cleanup_perch local fn).
- `src/live/stop.rs` (modified) — 1 resolver call (kill_psyche_wrapper D-09 symmetry).

## Decisions Made

- **Test-fixture migration is mandatory, not optional.** The naive interpretation of the plan ("migrate the reader call sites") would have silently broken the test fixtures whose writes landed flat. Discovered while reviewing each test that wrote a `<self>-psyche` perch. Added explicit fixture migration to Tasks 2, 3 (commune.rs `commune_result_bumps_cursor_on_offline_spool` + signoff.rs `signoff_result_bumps_cursor_when_psyche_reachable`) so the test perches now write to the nested path the resolver reads from.
- **stop.rs run_single/run_all not migrated** because the plan's speculative "additional sites" don't exist — grep confirmed only line 18 has an id-based perch composition in this file. Documented this in the Task 5 commit message.
- **boot_spine.rs cleanup_perch is called for BOTH Self ids and psyche ids** (lines 245-246). ParentHint::Infer handles both correctly via PerchKind::classify — Self returns flat (unchanged behavior), psyche returns nested (new correct behavior). The single migration covers both code paths.
- **Approach A picked for touch_loop.rs** over Approach B because scan_live_psyches has only one caller in the codebase (the touch loop body itself). Signature change ripple is zero outside the file. Matches the Phase 25 D-04 enumerate_perches pattern.

## Deviations from Plan

- **[Rule 2 — Auto-add missing critical functionality] Test fixture migration in commune.rs / signoff.rs**: the plan focused only on production reader sites, but the tests that exercise those readers wrote their psyche perches via flat `owlery::ready_file(&psyche_id)`. Post-migration the readers route through `resolve_ready_file(..., Explicit(self_id))` → nested → flat-written fixtures invisible → tests silently break. Migrated the fixture writes to the same resolver-routed paths so reads + writes agree. Same applies to cleanup.rs test helper + assertions per RESEARCH §E-8 ("benign today but fragile" — the migration eliminates the latent fragility).
- **[Rule 2] cleanup.rs migration scope clarified**: the plan referenced lines 167/171 as "per-perch teardown", but the actual code at those lines is the test `write_perch` helper. Production cleanup uses the walker's `&path` (already correct via Phase 25 D-04 inline-bumped walk). Migrated the test helper + four cleanup-test assertions to maintain symmetry; production code untouched.

## Authentication Gates

None. All work was local source code editing + cargo build/test on the working tree.

## Issues Encountered

- **None blocking.** The release lib-test sweep at end of Task 5 surfaced one pre-existing failure: `owl::version_changelog::tests::current_version_honors_override_in_debug_builds` (debug-build-only test invoked in release mode). Documented as pre-existing in Plan 25.4-03 SUMMARY; out of scope per per-task fix-attempt-limit.
- **Test-fixture migration was a Rule-2 auto-add.** Without it, the verify step would have failed for commune.rs and signoff.rs tests. Caught during Task 3 by tracing the fixture-write side after migrating the read side.

## Verification

- `cargo build --release` → succeeds (9 pre-existing warnings, none introduced by this plan).
- `cargo test --release --lib owl::send` → 0 tests (no module tests; functional verification via send-consumers below).
- `cargo test --release --lib owl::ring` → 0 tests (no module tests).
- `cargo test --release --lib owl::cleanup` → **4/4 pass**.
- `cargo test --release --lib live::commune -- --test-threads=1` → **5/5 pass**.
- `cargo test --release --lib live::signoff -- --test-threads=1` → **17/17 pass, 2 ignored** (ignored = 20s-budget tests, ignored by design per Phase 25.2 #3).
- `cargo test --release --lib live::touch_loop` → 0 tests (functional verification deferred to Plan 25.4-07 smoke probe).
- `cargo test --release --lib live::boot_spine` → **5/5 pass**.
- `cargo test --release --lib live::stop` → 0 tests (functional verification deferred to Plan 25.4-07 smoke probe).
- `cargo test --release --test poll_nested_psyche` → **1/1 pass** (Plan 03 invariant holds under Wave-3 readers).
- `cargo test --release --test wrapper_ready_file_landmine` → **2/2 pass** (Plan 02 invariant holds under Wave-3 readers).
- `cargo test --release --lib -- --test-threads=1` → **896 passed, 1 failed, 5 ignored**. The 1 failure is `owl::version_changelog::tests::current_version_honors_override_in_debug_builds` — pre-existing, documented in Plan 03 SUMMARY, out of scope.
- `grep -n "owlery::perch_dir\|owlery::ready_file\|owlery::info_file\|owlery::inbox_dir" src/owl/send.rs src/owl/ring.rs src/owl/cleanup.rs src/live/commune.rs src/live/touch_loop.rs src/live/boot_spine.rs src/live/stop.rs` → **0 matches**.
- `grep -n "owlery::perch_dir\|owlery::ready_file\|owlery::info_file\|owlery::inbox_dir" src/live/signoff.rs` → 8 matches, ALL on the Self-id arg `id` (lines 238/293 prod soft-stop teardown, lines 449/456/697/699 test fixtures, lines 746/757 in test helper `seed_perch_info_with_cwd`). Per plan: Self-kind resolves byte-for-byte identical under both APIs; leaving these unchanged minimizes churn.
- `grep -c "perch_path::resolve" src/{owl/send.rs,owl/ring.rs,owl/cleanup.rs,live/commune.rs,live/signoff.rs,live/boot_spine.rs,live/stop.rs}` → 9, 13, 9, 4, 5, 2, 1 — every migrated file has at least one resolver call.
- `grep -n "perch_path" src/live/touch_loop.rs` → 7 matches confirming the bubble-up pattern at scan_live_psyches signature + consumer.
- `live/stop.rs::kill_psyche_wrapper` and `live/wrapper/lifecycle.rs::cleanup` both clean nested — symmetric per CONTEXT D-09. Confirmed by grep: stop.rs:18 carries `perch_path::resolve_perch_path(&psid, ParentHint::Explicit(self_id))`; lifecycle.rs:127 was already nested per Plan 25.4-05.

## Self-Check: PASSED

- File `.planning/phases/25.4-perch-path-single-source-of-truth-finish-d-01-nested-migrati/25.4-06-SUMMARY.md` exists (this file).
- Commits `b16fdd9`, `e493348`, `8f3dbae`, `acb9aa6`, `355cd5f` present on main branch (verified via `git log --pretty=format:"%h" -7`).
- All eight modified source files compile cleanly (final `cargo build --release` succeeded).
- All success-criteria integration tests pass: `tests/wrapper_ready_file_landmine.rs` (2/2), `tests/poll_nested_psyche.rs` (1/1).

## Next Phase Readiness

- Plan 25.4-07 (on-disk smoke probe + cleanup-asymmetry resolution) is the final plan of Phase 25.4. With Plan 06 complete, every reader sees the same nested perch paths every writer writes. Plan 07's job is the end-to-end on-disk verification (no flat psyche dirs created during a live-start + commune + signoff cycle).
- No blockers.

---
*Phase: 25.4-perch-path-single-source-of-truth-finish-d-01-nested-migrati*
*Completed: 2026-05-23*
