---
phase: 25.4-perch-path-single-source-of-truth-finish-d-01-nested-migrati
plan: 05
subsystem: runtime
tags: [rust, perch-path, wrapper-state, binary-handoff, nested-migration, single-source-of-truth, deprecation]

requires:
  - phase: 25.4
    provides: "Plan 01 perch_path resolver module (resolve_perch_path / ParentHint / PerchKind); Plan 04 spool/inbox/sentinel writer migration consuming the same resolver"
provides:
  - "`src/common/wrapper_state.rs::wrapper_state_path_at(self_id, psyche_id)` explicit-parent NESTED writer-path resolver per CONTEXT D-03/D-04."
  - "`src/common/wrapper_state.rs::write_wrapper_state_at(self_id, psyche_id, state)` NESTED-canonical atomic writer."
  - "Legacy id-based `wrapper_state_path` + `write_wrapper_state` DEPRECATED and routed through `perch_path::resolve_perch_path(id, ParentHint::Infer)` — psyche ids now write nested (documented behavior change vs. pre-25.4 flat)."
  - "All three wrapper-side writer call sites (`claude.rs::init_session`, `lifecycle.rs::new` rehydration republish, `mod.rs::perform_wrapper_handoff`) migrated to the explicit `_at` form — wrapper-state.json files land nested-canonical including the binary-handoff path."
  - "Reader `wrapper_state_path_resolved` UNCHANGED — nested-first / flat-fallback for one deploy cycle's handoff transition (CONTEXT D-03 explicit deferral)."
affects: [25.4-06, 25.4-07]

tech-stack:
  added: []
  patterns:
    - "Explicit-parent `_at` writer naming convention parallels Plan 04's spool/inbox/sentinel migration — `write_wrapper_state` (Infer) deprecated; `write_wrapper_state_at(self_id, psyche_id, state)` (Explicit) preferred per CONTEXT D-04."
    - "Deprecated-but-routed-via-resolver pattern: the legacy id-based fn keeps compiling (zero ABI break for out-of-scope callers like `signoff.rs`/`start.rs` test fixtures) but its disk-layout output flips to nested via `ParentHint::Infer` — single source of truth even in the legacy code path."
    - "Reader/writer asymmetry retained intentionally: writers nested-canonical now, reader still dual-path for one deploy cycle. Symmetric removal scheduled for follow-up phase (out of scope for 25.4 per CONTEXT D-03)."

key-files:
  created: []
  modified:
    - src/common/wrapper_state.rs
    - src/live/wrapper/claude.rs
    - src/live/wrapper/lifecycle.rs
    - src/live/wrapper/mod.rs

key-decisions:
  - "Reader `wrapper_state_path_resolved` UNCHANGED per CONTEXT D-03 — one deploy cycle's safety margin for the handoff transition (gen-N-1 wrapper hands off to gen-N binary; the gen-N wrapper reads gen-N-1-written flat file once, then republishes nested on next iteration). Follow-up phase removes the flat-fallback branch."
  - "`lifecycle.rs::new` uses `self_id` directly (the `new()` parameter) instead of deriving via psyche_id strip-suffix — `self_id` is in scope at the call site, zero-ambiguity per CONTEXT D-04 explicit-first."
  - "`wrapper/mod.rs::perform_wrapper_handoff` adds defensive `create_dir_all(state_file.parent())` before `write_atomic` — the nested perch dir may not exist if no prior writer materialized it. Mirrors the pattern in the new `write_wrapper_state_at` helper."
  - "Legacy `signoff.rs:512`, `signoff.rs:652`, `start.rs:907` calls to `write_wrapper_state` are OUT OF SCOPE — they live in `#[cfg(test)]` test fixtures (so they don't produce deprecation warnings in `cargo build --release`), and the plan's `must_haves.truths` explicitly scopes this to the THREE wrapper-side writer sites. Follow-up plan migrates the test fixtures."
  - "Test `wrapper_state_path_matches_load_and_delete_path` updated to pin the new NESTED invariant — previously asserted FLAT equality with `owlery::perch_dir(agent_id).join(...)`; now asserts NESTED equality with `owlery::nested_perch_dir(self_id, agent_id).join(...)` AND `wrapper_state_path_at(self_id, agent_id)` agreement. The behavior change is intentional + documented in the deprecation note."
  - "Pre-existing test interaction failures in `cargo test --lib live::wrapper` (10 failures, all unrelated to wrapper-state) confirmed pre-existing by stashing changes + re-running on `main` (same 10 failures). Logged in `.planning/phases/25.4-.../deferred-items.md`; deferred to a follow-up debug/quick. Per-module test invocations for the directly-modified files (`live::wrapper::lifecycle`, `live::wrapper::claude`) pass clean."
  - "Pre-existing `tests/native_wrapper_state_retry.rs` build error (3 `WrapperHandoffState` literals missing `pulse_psyche`) NOT auto-fixed — it's integration-test scope, the plan's verify surfaces don't touch it, and Plan 25.4-04's SUMMARY already flagged it for a separate follow-up. Logged in deferred-items.md."

patterns-established:
  - "Pattern: when deprecating an id-based path resolver, route the deprecated form through `resolve_perch_path(id, ParentHint::Infer)` so its on-disk semantics align with the new explicit form. Callers that haven't migrated yet still produce nested files — the deprecation is a soft migration, not a behavior split."
  - "Pattern: comment blocks at every migrated writer site cite the controlling CONTEXT decision (`Phase 25.4 D-03: migration-window CLOSED. ...`) so a future reader of the call site immediately sees the architectural justification without grepping CONTEXT.md."

requirements-completed: [PERCH-PATH-WRAPPER-STATE-MIGRATION-01]

duration: ~15min
completed: 2026-05-24
---

# Phase 25.4 Plan 05: wrapper-state writer migration Summary

**Closed the `wrapper-state.json` flat-writer migration window per CONTEXT D-03. All three wrapper-side writers (`claude.rs::init_session` post-init publish, `lifecycle.rs::new` handoff-rehydration republish, `mod.rs::perform_wrapper_handoff` binary-handoff state-file composition) now produce NESTED-canonical files at `owlery/<self>/nested/<psyche>/wrapper-state.json`. Reader `wrapper_state_path_resolved` left unchanged (nested-first / flat-fallback) for one deploy cycle's handoff transition; removal scheduled for follow-up phase.**

## Performance

- **Duration:** ~15 min
- **Tasks:** 3 (committed as 3 atomic commits per task-per-commit structure)
- **Files modified:** 4

## Accomplishments

- `src/common/wrapper_state.rs`: added `wrapper_state_path_at(self_id, psyche_id) -> PathBuf` routing through `perch_path::resolve_perch_path(psyche_id, ParentHint::Explicit(self_id))`. Added `write_wrapper_state_at(self_id, psyche_id, &state) -> io::Result<()>` composing on top. Deprecated legacy `wrapper_state_path(agent_id)` and `write_wrapper_state(agent_id, &state)`; both now route via `ParentHint::Infer` (behavior change: psyche ids land nested instead of flat — documented in deprecation note). Reader `wrapper_state_path_resolved` UNCHANGED.
- `src/live/wrapper/claude.rs` line 201: `wrapper_state_helper::write_wrapper_state(&self.psyche_id, &state)` → `wrapper_state_helper::write_wrapper_state_at(&self.self_id, &self.psyche_id, &state)`. Phase 25.2 comment block updated to Phase 25.4 D-03 wording.
- `src/live/wrapper/lifecycle.rs` line 103: `wrapper_state::write_wrapper_state(&psyche_id, &published)` → `wrapper_state::write_wrapper_state_at(self_id, &psyche_id, &published)`. `self_id` already in scope as the `new()` parameter — zero-ambiguity explicit-parent.
- `src/live/wrapper/mod.rs` line 1693 (formerly 1685): `let state_file = owlery::perch_dir(&self.psyche_id).join("wrapper-state.json");` → `let state_file = wrapper_state::wrapper_state_path_at(&self.self_id, &self.psyche_id);`. Added defensive `create_dir_all(state_file.parent())` before `write_atomic` so the nested perch dir exists for tmp+rename. Comment block updated.
- 4 new lib tests in `wrapper_state::tests`:
  - `wrapper_state_path_at_returns_nested_path` — explicit-parent form returns nested layout.
  - `write_wrapper_state_at_roundtrips_via_resolver` — `_at` write + `wrapper_state_path_resolved` read agree on the nested path.
  - `legacy_wrapper_state_path_for_psyche_id_now_returns_nested` — pins the deprecation-note behavior change.
  - `reader_still_finds_flat_only_state_for_handoff_transition` — proves the reader's flat-fallback branch still works for gen-N-1 → gen-N handoff.
- 1 updated lib test: `wrapper_state_path_matches_load_and_delete_path` — flipped from FLAT to NESTED invariant; also pins agreement between deprecated `wrapper_state_path` (Infer) and new `wrapper_state_path_at` (Explicit).

## Task Commits

1. **Task 1: `wrapper_state_path_at` + `write_wrapper_state_at` + deprecate legacy id-based fns** — `6e49fe2` (feat)
2. **Task 2: migrate `claude.rs:201` + `lifecycle.rs:103` to `write_wrapper_state_at`** — `b4b7dea` (feat)
3. **Task 3: migrate `mod.rs::perform_wrapper_handoff` to `wrapper_state_path_at`** — `9e8955e` (feat)

_Each task committed atomically per the plan's task-per-commit structure. No batching needed — each commit builds and passes its verification surface independently._

## Files Created/Modified

- `src/common/wrapper_state.rs` (modified): +191 / -7 lines. New `_at` writer surface + deprecation of legacy id-based fns + 4 new unit tests + 1 updated existing test + reader doc-comment updated to cite Phase 25.4 D-03 (writers nested-canonical, reader dual-path for one deploy cycle).
- `src/live/wrapper/claude.rs` (modified): +12 / -10 lines. Post-init publish call migrated; comment block updated.
- `src/live/wrapper/lifecycle.rs` (modified): +9 / -10 lines. Handoff-rehydration republish call migrated; comment block updated.
- `src/live/wrapper/mod.rs` (modified): +15 / -7 lines. Handoff state-file composition migrated + defensive parent-dir create + comment block updated.
- `.planning/phases/25.4-.../deferred-items.md` (created): logs the pre-existing test failures and the `tests/native_wrapper_state_retry.rs` build error.

## Decisions Made

- **Reader resolver UNCHANGED in this plan**: per CONTEXT D-03, the `wrapper_state_path_resolved` flat-fallback branch covers the one-deploy-cycle handoff transition. A gen-N wrapper that takes over from a gen-N-1 wrapper reads the gen-N-1-written flat file once, then republishes nested on next iteration. Removal of the flat-fallback branch is explicitly scheduled for a follow-up phase (out of scope for 25.4 per CONTEXT Deferred Ideas).
- **Legacy `wrapper_state_path(psyche_id)` behavior change**: previously returned FLAT `owlery/<psyche_id>/wrapper-state.json`; now returns NESTED `owlery/<self_id>/nested/<psyche_id>/wrapper-state.json` via `ParentHint::Infer` (strip `-psyche` suffix). This IS a documented behavior change — the deprecation note calls it out explicitly. It matches the migration intent: a single source of truth for the layout, even in the legacy code path. Existing `#[cfg(test)]` callers in `signoff.rs` / `start.rs` will write nested transparently.
- **`lifecycle.rs::new` uses `self_id` directly, not strip-suffix**: the `new(self_id, ...)` parameter is in scope; preferring the explicit form over `psyche_id.trim_end_matches("-psyche")` keeps the migration mechanical and matches CONTEXT D-04 explicit-first.
- **Defensive `create_dir_all` in `perform_wrapper_handoff`**: matches the pattern in the new `write_wrapper_state_at` helper. Steady-state perches exist on disk (start.rs creates them), but the handoff path runs in an unusual lifecycle moment — defensive is cheap and aligns the three writer sites on identical pre-write behavior.
- **Pre-existing test failures NOT auto-fixed**: 10 failures in `cargo test --lib live::wrapper` are pre-existing on `main` (verified via stash + re-run). They live in `file_drop_handler_tests`, `lifecycle::tests::drain_stale_init_signoffs_*`, `claude::tests::emit_inwrapper_boot_seals_on_second_call`, and `phase_25_3_invariant_inbound_two_slice_envelope_persists_correctly` — none touch wrapper-state path resolution. Per SCOPE BOUNDARY rule, logged to `deferred-items.md` and not fixed. The directly-modified test surfaces (`live::wrapper::lifecycle`, `live::wrapper::claude`, `common::wrapper_state`) all pass clean when invoked per-module.
- **`tests/native_wrapper_state_retry.rs` build error NOT auto-fixed**: pre-existing 3-literal compile error (missing `pulse_psyche` field) flagged by Plan 25.4-04's SUMMARY. Out of scope for this plan's verify surfaces. Logged in deferred-items.md.

## Deviations from Plan

- **Added defensive `create_dir_all(state_file.parent())` in `perform_wrapper_handoff`** (mod.rs Task 3): plan didn't explicitly require this, but the migration moves the state file to a nested path that may not exist on disk (the legacy flat path was auto-created as a side effect of spool/inbox writers — that side effect is now gone per Plan 25.4-04). Rule 2 (auto-add missing critical functionality): without this, `write_atomic`'s tmp+rename would fail on first-handoff if the nested perch dir doesn't exist. The new `write_wrapper_state_at` helper already does this internally; matching it at the inline `write_atomic` call site keeps the three writer sites symmetric.
- **TDD red/green cycles collapsed to single commits per task**: tests and implementation written together; both run at commit time. Matches the pattern established in Plans 01/02/03/04 of this phase (test scaffold + impl tightly coupled — a RED-first commit would leave non-building code).

## Issues Encountered

- **Pre-existing test interaction failures** (10 tests in `cargo test --lib live::wrapper`) — confirmed pre-existing via `git stash` + re-run on `main`. Same 10 failures both with and without my changes. Per SCOPE BOUNDARY rule, not auto-fixed; logged in `.planning/phases/25.4-.../deferred-items.md`.
- **Pre-existing build error in `tests/native_wrapper_state_retry.rs`** (3 `WrapperHandoffState` literals missing `pulse_psyche`) — Plan 25.4-04 SUMMARY already flagged this. The plan's verify surfaces don't touch this test, so not blocking. Logged in deferred-items.md.

## Verification

- `cargo build --release` → succeeds; only pre-existing warnings.
- `cargo build --release 2>&1 | grep -i "deprecated" | grep -E "(claude\\.rs|lifecycle\\.rs)"` → **0 matches** (Task 2 success criterion).
- `cargo test --lib common::wrapper_state` → **15 passed; 0 failed; 0 ignored** (Task 1 success criterion; 4 new tests + 1 updated test included).
- `cargo test --lib live::wrapper::lifecycle` → **5 passed; 0 failed** (Task 2 surface).
- `cargo test --lib live::wrapper::claude` → **2 passed; 0 failed** (Task 2 surface).
- `cargo test --test handoff_integration` → **19 passed; 0 failed; 1 ignored** (Task 3 success criterion — binary-handoff state file still readable post-migration).
- `cargo test --test wrapper_ready_file_landmine` → **2 passed; 0 failed** (Wave 1 landmine invariant intact).
- `cargo test --test poll_nested_psyche` → **1 passed; 0 failed** (Plan 03 nested-write contract intact).
- `grep -rn "wrapper_state::write_wrapper_state\\b\\|wrapper_state_helper::write_wrapper_state\\b" src/live/wrapper/` → **0 matches** (every wrapper-side call site migrated to `_at`).
- `grep -n "owlery::perch_dir(&self\\.psyche_id)" src/live/wrapper/mod.rs` → **0 matches** (line 1685 migrated).
- `grep -n "wrapper_state_path_at\\|write_wrapper_state_at" src/live/wrapper/` → **6 matches** (3 call sites + 3 doc-comment refs) — meets the plan's "3 matches" floor (the extra 3 are comment citations).
- `wrapper_state_path_resolved` reader UNCHANGED — verified via `git diff src/common/wrapper_state.rs` shows additions only in writer section + doc-comment update in reader section.

## Self-Check: PASSED

- File `src/common/wrapper_state.rs` modified — verified via grep showing `wrapper_state_path_at` and `write_wrapper_state_at` present.
- File `src/live/wrapper/claude.rs` modified — verified via grep showing `write_wrapper_state_at` at line 199.
- File `src/live/wrapper/lifecycle.rs` modified — verified via grep showing `write_wrapper_state_at` at line 103.
- File `src/live/wrapper/mod.rs` modified — verified via grep showing `wrapper_state_path_at` at line 1696.
- Commit `6e49fe2` present on main (verified via `git log --oneline -5`).
- Commit `b4b7dea` present on main (verified via `git log --oneline -5`).
- Commit `9e8955e` present on main (verified via `git log --oneline -5`).
- Both regression tests (`wrapper_ready_file_landmine`, `poll_nested_psyche`) passing post-migration — plan-level success criteria #5 + #6.

## Threat Flags

None — no new network endpoints, auth paths, file access patterns, or trust-boundary surfaces. The migration consolidates wrapper-state writes to a single nested path per psyche, narrowing the on-disk footprint and aligning with the threat-model expectation that psyche state lives under its Self parent (parent-cascade cleanup, single-owner invariant per Phase 25 D-01).

## Next Phase Readiness

- Plan 25.4-06 (Class E reader migration) is unblocked. It will consume the same `perch_path` resolver from `commune.rs`, `signoff.rs`, `touch_loop.rs`, `boot_spine.rs`, `stop.rs`, `send.rs`, `ring.rs`, and (per CONTEXT D-08) flip the `.more-done` reader (`echo_fire.rs::pick_fresher_more_done`) to drop its dual-path fallback. The `wrapper_state_path_resolved` flat-fallback branch is INTENTIONALLY left in place per CONTEXT D-03 (one more deploy cycle); its removal is a separate follow-up phase, not Plan 25.4-06.
- Plan 25.4-07 (on-disk smoke probe + cleanup-asymmetry resolution) can verify the end-state: after a fresh live boot + binary handoff cycle, no new flat `wrapper-state.json` files appear on disk under `owlery/<psyche-id>/`.
- Follow-up work surfaced during execution:
  - Migrate `signoff.rs:512`, `signoff.rs:652`, `start.rs:907` (`#[cfg(test)]` fixtures) from `write_wrapper_state` → `write_wrapper_state_at` to silence deprecation warnings under `cargo test`. Currently warnings only fire on `cargo test`, not `cargo build --release`.
  - Fix `tests/native_wrapper_state_retry.rs` (3 `WrapperHandoffState` literals missing `pulse_psyche`). Pre-existing; flagged by Plan 25.4-04 SUMMARY.
  - Investigate the 10 pre-existing `cargo test --lib live::wrapper` failures (worktree setup collisions, file-drop handler, seal-commit ordering, init-signoff drain, 25.3 invariant). Logged in deferred-items.md.

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