---
phase: 25.4-perch-path-single-source-of-truth-finish-d-01-nested-migrati
verified: 2026-05-24T00:00:00Z
status: passed
score: 14/14
overrides_applied: 0
re_verification: null
gaps: []
deferred: []
human_verification: []
---

# Phase 25.4: Perch-Path Single Source of Truth — Verification Report

**Phase Goal:** Finish Phase 25 D-01 nested migration end-to-end for psyche/worker perch state. Introduce central perch-path resolver as SINGLE source of truth. Route every writer (poll.rs, spool.rs, inbox.rs, hook_check.rs .owl-aware, hook_idle.rs .more-done, wrapper-state writers) and every Class-E reader (send.rs, ring.rs, cleanup.rs, commune.rs, signoff.rs, touch_loop.rs, boot_spine.rs, stop.rs) through the resolver. Close wrapper-state.json flat-writer migration window per CONTEXT D-03. Verify end-to-end via automated integration tests + operator-driven on-disk smoke probe.

**Verified:** 2026-05-24
**Status:** PASSED
**Re-verification:** No — initial verification

---

## Goal Achievement

### Observable Truths

| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Central resolver `resolve_perch_path` + `PerchKind` + `ParentHint` exists in `src/common/perch_path.rs` as the single source of truth for perch path composition | VERIFIED | File exists, 438 lines, exports all specified types and functions; `pub mod perch_path` in `src/common/mod.rs` line 21 |
| 2 | `PerchKind::classify` returns correct kinds for all id suffix patterns; Worker rule uses rightmost `-w{N}` (`rfind`), matching `owlery::is_worker_perch` byte-for-byte | VERIFIED | Full classify implementation confirmed; pinning unit test `classify_worker_agrees_with_owlery_is_worker_perch` covers 11 representative ids |
| 3 | `ParentHint::Explicit` wins over Infer; Infer strips kind suffix; `Self_/Spine/Touch` return flat path regardless of hint | VERIFIED | `resolve_perch_path` implementation verified; 8 resolution unit tests including `resolve_explicit_wins_over_infer` cover all three hint paths |
| 4 | Companion helpers (`resolve_ready_file`, `resolve_info_file`, `resolve_inbox_dir`, `resolve_perch_root`) compose on the resolver | VERIFIED | All four helpers present and delegate to `resolve_perch_path`; 4 child-file helper unit tests cover each |
| 5 | `poll.rs` routes all perch-path composition through resolver via `--parent` flag; Self listeners unaffected | VERIFIED | `src/owl/poll.rs` imports `perch_path::{self, ParentHint}`; uses `resolve_perch_path`, `resolve_ready_file`, `resolve_info_file` with `ParentHint::Explicit(parent)` / `Infer`; zero bare `owlery::perch_dir(id)` / `owlery::ready_file(id)` calls in poll.rs |
| 6 | Psyche wrapper passes `--parent <self_id>` to inner poll subprocess | VERIFIED | `src/live/wrapper/mod.rs` line 1539: `args.push("--parent"); args.push(&self.self_id)` |
| 7 | `spool.rs` gains `open_spool_at` + `_at` variants; legacy id-based fns delegate via resolver; `inbox.rs` gains parallel `_at` surface | VERIFIED | `open_spool_at`, `spool_message_at`, `drain_one` etc. confirmed in `src/common/spool.rs`; `set_has_messages_at`, `write_to_inbox_at`, `drain_inbox_at`, `set_idle_ready_at` etc. confirmed in `src/common/inbox.rs` |
| 8 | `.owl-aware` sentinel writer in `hook_check.rs` uses perch_path bubbled from `find_working_perch_with_parent` 3-tuple | VERIFIED | `src/common/hook_output.rs` returns `Option<(String, Option<String>, PathBuf)>`; `src/owl/hook_check.rs` line 150: `let sentinel = perch_path.join(".owl-aware")` using the returned path |
| 9 | `.more-done` sentinel writer in `hook_idle.rs` is nested-canonical via `resolve_perch_path(psyche_id, Explicit(owl_id))`; gate predicate also uses nested resolver (finishing fix `48b61be`) | VERIFIED | `src/owl/hook_idle.rs` line 144: `perch_path::resolve_ready_file(&psyche_id, ParentHint::Explicit(owl_id)).exists()`; line 160: `perch_path::resolve_perch_path(&psyche_id, ParentHint::Explicit(owl_id))`; version-change early-return dropped per commit `72b4263` |
| 10 | Wrapper-state.json writers all flip to nested via `write_wrapper_state_at(self_id, psyche_id, state)`; reader `wrapper_state_path_resolved` unchanged | VERIFIED | `src/live/wrapper/claude.rs:199`: `write_wrapper_state_at`; `src/live/wrapper/lifecycle.rs:103`: `write_wrapper_state_at`; `src/live/wrapper/mod.rs:1696`: `wrapper_state_path_at`; reader at `wrapper_state.rs` unchanged |
| 11 | All 8 Class-E reader sites (`send.rs`, `ring.rs`, `cleanup.rs`, `commune.rs`, `signoff.rs`, `touch_loop.rs`, `boot_spine.rs`, `stop.rs`) route through resolver | VERIFIED | Every file confirmed via grep: send.rs (9 resolver calls), ring.rs (10 resolver calls), cleanup.rs (2 resolver calls), commune.rs (2 explicit resolver calls), signoff.rs (2 explicit resolver calls), touch_loop.rs (`scan_live_psyches` returns `Vec<(String, PathBuf)>` from `enumerate_perches`), boot_spine.rs (2 resolver calls with Infer), stop.rs (resolver call at `kill_psyche_wrapper` line 25) |
| 12 | `stop.rs::kill_psyche_wrapper` cleans nested psyche perch via `Explicit(self_id)` — symmetric with `wrapper/lifecycle.rs:127` | VERIFIED | `stop.rs` line 25: `perch_path::resolve_perch_path(&psid, ParentHint::Explicit(self_id))`; `lifecycle.rs:127`: `owlery::nested_perch_dir(&self.self_id, &self.psyche_id)` — both produce the same nested path |
| 13 | Automated integration tests (9 total across 4 files) all exist and pass: `wrapper_ready_file_landmine.rs` (2), `poll_nested_psyche.rs` (1), `perch_path_no_flat_psyche.rs` (2), `perch_path_no_flat_worker.rs` (4) | VERIFIED | All 4 test files present; test counts confirmed via `#[test]` grep; per operator and SUMMARY every test passes (`cargo test` green) |
| 14 | On-disk smoke probe (operator-run post-deploy) confirms zero new flat psyche/worker dirs for fresh test agent `probetest`; all writes nested-only | VERIFIED | `25.4-SMOKE-PROBE.md` populated with post-deploy output: `=== FLAT PSYCHE DIRS ===` empty, `=== FLAT WORKER DIRS (-w[N]) ===` empty, `probetest/nested/probetest-psyche/` has all expected files with fresh mtimes; operator verdict: PASS |

**Score:** 14/14 truths verified

---

## Required Artifacts

| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/common/perch_path.rs` | Central perch-path resolver module; `resolve_perch_path` + `PerchKind` + `ParentHint` + 4 helpers + 24 unit tests | VERIFIED | 438 lines; all types and functions present; 24 tests confirmed via `#[test]` count |
| `src/common/mod.rs` | `pub mod perch_path` registered | VERIFIED | Line 21: `pub mod perch_path;` |
| `tests/wrapper_ready_file_landmine.rs` | 2 tests pinning `wrapper/mod.rs:1168` ready-file invariant | VERIFIED | File present; 2 tests with provenance comments citing CONTEXT D-06, RESEARCH Open Q7, and `wrapper/mod.rs:1168` |
| `src/cli.rs` | `--parent: Option<String>` on `Commands::Poll` | VERIFIED | Line 47: `parent: Option<String>` |
| `src/owl/poll.rs` | Resolver-routed perch-path composition via `--parent` | VERIFIED | `resolve_perch_path`, `resolve_ready_file`, `resolve_info_file` with `ParentHint::Explicit/Infer` |
| `src/live/wrapper/mod.rs` | `poll_psyche` passes `--parent <self_id>` | VERIFIED | Lines 1539-1540 |
| `tests/poll_nested_psyche.rs` | Integration test: poll with `--parent` writes nested-only | VERIFIED | 1 test present |
| `src/common/spool.rs` | `open_spool_at` + full `_at` surface | VERIFIED | `open_spool_at`, all `_at` variants confirmed |
| `src/common/inbox.rs` | Full `_at` surface (9 functions) | VERIFIED | `drain_inbox_at`, `write_to_inbox_at`, `set_idle_ready_at`, `set_has_messages_at` + others confirmed |
| `src/common/hook_output.rs` | `find_working_perch_with_parent` returns 3-tuple `Option<(String, Option<String>, PathBuf)>` | VERIFIED | Signature confirmed |
| `src/owl/hook_check.rs` | `.owl-aware` uses bubbled-up `perch_path` | VERIFIED | Line 150: `perch_path.join(".owl-aware")` |
| `src/owl/hook_idle.rs` | `.more-done` writer nested-canonical; gate predicate nested-resolver; version-change early-return dropped | VERIFIED | Lines 144 + 160 confirmed; `let _ = version_changelog::...` without return |
| `src/common/wrapper_state.rs` | `wrapper_state_path_at` + `write_wrapper_state_at`; legacy fns deprecated; reader unchanged | VERIFIED | All confirmed present |
| `src/live/wrapper/claude.rs` | `write_wrapper_state_at` call at line ~199 | VERIFIED | Confirmed |
| `src/live/wrapper/lifecycle.rs` | `write_wrapper_state_at` call at line ~103 | VERIFIED | Confirmed |
| `src/live/wrapper/mod.rs` | `wrapper_state_path_at` at `perform_wrapper_handoff` line ~1696 | VERIFIED | Confirmed |
| `src/owl/send.rs` | All target-path compositions via `perch_path::resolve_*` with `ParentHint::Infer` | VERIFIED | 9 resolver call sites; zero bare `owlery::perch_dir(id)` / `owlery::ready_file(id)` |
| `src/owl/ring.rs` | All target-path compositions via resolver | VERIFIED | 10 resolver call sites; zero bare id-based calls |
| `src/owl/cleanup.rs` | id-derived perch paths via resolver | VERIFIED | 2 resolver calls at lines 172/176 |
| `src/live/commune.rs` | Psyche readiness via `resolve_ready_file` with `Explicit(self_id)` | VERIFIED | Lines 66, 128 |
| `src/live/signoff.rs` | Psyche readiness via resolver with `Explicit(id)` | VERIFIED | Lines 179, 260 |
| `src/live/touch_loop.rs` | `scan_live_psyches` returns `Vec<(String, PathBuf)>` from `enumerate_perches` | VERIFIED | Lines 24-36: returns 3-tuple's path; loop at line 91 uses `(psyche_id, perch_path)` |
| `src/live/boot_spine.rs` | PSYCHE_DEAD revival re-check + `cleanup_perch` via resolver with `Infer` | VERIFIED | Lines 225, 288 |
| `src/live/stop.rs` | `kill_psyche_wrapper` via `Explicit(self_id)` | VERIFIED | Line 25 |
| `tests/perch_path_no_flat_psyche.rs` | 2 tests: nested-only psyche; Self listener flat | VERIFIED | File present; 2 tests confirmed |
| `tests/perch_path_no_flat_worker.rs` | 4 tests: spool/inbox nested-only for workers; Self preserved flat | VERIFIED | File present; 4 tests confirmed |
| `.planning/phases/25.4-.../25.4-SMOKE-PROBE.md` | Pre/post-deploy probe with PASS verdict | VERIFIED | File present with PASS verdict, operator diff annotations, and finishing-fix RCA |

---

## Key Link Verification

| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `src/live/wrapper/mod.rs::poll_psyche` | `src/owl/poll.rs::run` | `--parent <self_id>` in argv | VERIFIED | Line 1539-1540 in wrapper pushes `--parent` flag; `poll.rs` dispatches via `ParentHint::Explicit` |
| `src/owl/poll.rs::run` | `src/common/perch_path.rs::resolve_perch_path` | All perch-path composition routes through resolver | VERIFIED | `resolve_perch_path`, `resolve_ready_file`, `resolve_info_file` calls confirmed |
| `src/common/spool.rs::spool_message_at` | `src/common/inbox.rs::set_has_messages_at` | Symmetric path threading — spool writes and inbox sentinel at same perch_path | VERIFIED | `spool.rs` line 91: `inbox::set_has_messages_at(perch_path)` |
| `src/owl/hook_check.rs` | `src/common/hook_output.rs::find_working_perch_with_parent` | 3-tuple return surfaces `perch_path` for `.owl-aware` sentinel | VERIFIED | `hook_check.rs` line 150 uses `perch_path.join(".owl-aware")` from 3-tuple |
| `src/owl/hook_idle.rs` | `src/common/perch_path.rs` | `.more-done` writer resolves psyche path via Explicit(owl_id) | VERIFIED | `hook_idle.rs` line 160: `perch_path::resolve_perch_path(&psyche_id, ParentHint::Explicit(owl_id))` |
| `src/live/wrapper/claude.rs::init_session` | `src/common/wrapper_state.rs::write_wrapper_state_at` | Nested-canonical wrapper-state write | VERIFIED | `claude.rs` line 199 |
| `src/live/wrapper/mod.rs::perform_wrapper_handoff` | `src/common/wrapper_state.rs::wrapper_state_path_at` | Handoff uses same nested path as steady-state | VERIFIED | `mod.rs` line 1696 |
| `src/common/wrapper_state.rs::wrapper_state_path_at` | `src/common/perch_path.rs::resolve_perch_path` | Explicit-parent resolver call — psyche kind nests under self_id | VERIFIED | Confirmed via grep |
| `src/live/stop.rs::kill_psyche_wrapper` | `src/common/perch_path.rs::resolve_perch_path` | Explicit-parent resolution; symmetric with lifecycle.rs:127 | VERIFIED | `stop.rs` line 25; `lifecycle.rs:127` uses `owlery::nested_perch_dir` — both produce identical nested path |
| `src/live/commune.rs:66` | `src/common/perch_path.rs::resolve_ready_file` | Psyche readiness check via Explicit(self_id) | VERIFIED | `commune.rs` line 66 |

---

## Data-Flow Trace (Level 4)

This phase does not render dynamic UI data — it routes path composition through a central resolver. The smoke probe provides the data-flow verification: actual on-disk file locations observed post-deploy confirm the data flows through the resolver at runtime (nested paths created, no flat psyche/worker side-effects). Level 4 trace deferred to behavioral spot-check and probe.

---

## Behavioral Spot-Checks

| Behavior | Check | Result | Status |
|----------|-------|--------|--------|
| Psyche poll with `--parent` writes nested-only | `tests/perch_path_no_flat_psyche.rs::psyche_poll_with_parent_writes_nested_only` | 2 tests pass per SUMMARY | PASS |
| Self listener without `--parent` stays flat | `tests/perch_path_no_flat_psyche.rs::psyche_poll_without_parent_preserves_self_listener_flat` | Passes per SUMMARY | PASS |
| Worker spool/inbox auto-create routes nested | `tests/perch_path_no_flat_worker.rs` (4 tests) | 4 tests pass per SUMMARY | PASS |
| Live machine probe: zero new flat psyche/worker dirs | Operator ran `probe.ps1` post-deploy v1.11.10→11→12; `=== FLAT PSYCHE DIRS ===` empty, `=== FLAT WORKER DIRS ===` empty | PASS — operator verdict in SMOKE-PROBE.md | PASS |

---

## Probe Execution

| Probe | Command | Result | Status |
|-------|---------|--------|--------|
| Operator live-machine smoke probe | `probe.ps1` run on `%LOCALAPPDATA%\spt\owlery` after `$LIVE start probetest` + subagent spawn | Post-deploy: flat psyche section empty; flat worker section empty; `probetest/nested/probetest-psyche/` has all expected files with fresh mtimes. Operator verdict: PASS | PASS |

(No automated `scripts/*/tests/probe-*.sh` in this project — probe is PowerShell-based, operator-run, and result is captured in `25.4-SMOKE-PROBE.md`.)

---

## Requirements Coverage

| Requirement | Plan | Description | Status | Evidence |
|-------------|------|-------------|--------|----------|
| PERCH-PATH-RESOLVER-01 | 25.4-01 | `resolve_perch_path` function exists | SATISFIED | `src/common/perch_path.rs` exports `resolve_perch_path` |
| PERCH-PATH-RESOLVER-02 | 25.4-01 | `PerchKind::classify` correct for all id patterns + `nests_under_parent` predicate | SATISFIED | classify + nests_under_parent verified in source; 13+ classification tests |
| PERCH-PATH-RESOLVER-03 | 25.4-01 | `ParentHint::Explicit/Infer` + 4 child-file helpers + unit tests | SATISFIED | All present; 24 tests confirmed |
| PERCH-PATH-LANDMINE-01 | 25.4-02 | `tests/wrapper_ready_file_landmine.rs` pins wrapper/mod.rs:1168 invariant | SATISFIED | File present, 2 tests, provenance comments correct |
| PERCH-PATH-POLL-MIGRATION-01 | 25.4-03 | `poll.rs` `--parent` flag + resolver-routed path composition | SATISFIED | `src/cli.rs` `parent: Option<String>`; poll.rs resolver calls confirmed |
| PERCH-PATH-POLL-MIGRATION-02 | 25.4-03 | Wrapper passes `--parent <self_id>`; integration test confirms nested-only side effects | SATISFIED | `mod.rs:1539-1540`; `tests/poll_nested_psyche.rs` passes |
| PERCH-PATH-SPOOL-MIGRATION-01 | 25.4-04 | `open_spool_at` + full `_at` surface in `spool.rs`; legacy id-based fns delegate via resolver | SATISFIED | All `_at` variants confirmed; legacy fns use `resolve_perch_path(id, Infer)` |
| PERCH-PATH-INBOX-MIGRATION-01 | 25.4-04 | Full `_at` surface in `inbox.rs`; legacy fns delegate via resolver | SATISFIED | All `_at` variants confirmed |
| PERCH-PATH-SENTINEL-MIGRATION-01 | 25.4-04 | `.owl-aware` uses bubbled perch_path; `.more-done` nested-canonical; gate predicate also nested (finishing fix) | SATISFIED | `hook_check.rs:150`, `hook_idle.rs:144+160` confirmed |
| PERCH-PATH-WRAPPER-STATE-MIGRATION-01 | 25.4-05 | Three wrapper-state writers → `write_wrapper_state_at`; reader unchanged | SATISFIED | All three writer sites confirmed; `wrapper_state_path_resolved` unchanged |
| PERCH-PATH-READER-MIGRATION-01 | 25.4-06 | `send.rs`, `ring.rs`, `cleanup.rs` reader sites via resolver with `Infer` | SATISFIED | Zero bare `owlery::perch_dir/ready_file/info_file` calls in these files |
| PERCH-PATH-READER-MIGRATION-02 | 25.4-06 | `commune.rs`, `signoff.rs`, `touch_loop.rs`, `boot_spine.rs` reader sites via resolver | SATISFIED | All confirmed; `touch_loop` uses `enumerate_perches` path bubbling (Approach A) |
| PERCH-PATH-STOP-SYMMETRY-01 | 25.4-06 | `stop.rs::kill_psyche_wrapper` and `lifecycle.rs:127` symmetric nested-cleanup | SATISFIED | `stop.rs:25` Explicit(self_id); `lifecycle.rs:127` `nested_perch_dir(&self.self_id, &self.psyche_id)` — identical output path |
| PERCH-PATH-SMOKE-PROBE-01 | 25.4-07 | Operator post-deploy probe confirms zero new flat dirs; `25.4-SMOKE-PROBE.md` populated | SATISFIED | SMOKE-PROBE.md present with PASS verdict, operator diff annotations |
| PERCH-PATH-WORKER-MYSTERY-01 | 25.4-07 | Worker-mystery (RESEARCH Open Q1) resolution documented | SATISFIED | SMOKE-PROBE.md documents: post-migration zero flat worker side-effects confirmed; one-shot subagent `spawn_worker_perch` reliability carried forward as orthogonal investigation per CONTEXT D-07 — this is the documented disposition, not a miss |
| PERCH-PATH-GOAL-VERIFY-01 | 25.4-07 | `perch_path_no_flat_psyche.rs` + `perch_path_no_flat_worker.rs` automated tests + smoke probe PASS | SATISFIED | Both test files present with correct test counts; smoke probe PASS |

---

## Anti-Patterns Found

| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `src/live/signoff.rs` | 238, 293 | `owlery::perch_dir(id)` call | INFO | These are Self perch teardowns — `id` is the caller's own Self id, not a psyche/worker id. `PerchKind::Self_` returns flat path; resolver would produce identical result. Correctly left using the direct call (Self teardown is flat by definition). Not a regression. |
| `deferred-items.md` | — | Pre-existing test failures in `live::wrapper` (10 tests) | INFO | Pre-existing on `main` before Phase 25.4; explicitly logged and deferred. Not introduced by Phase 25.4. |

No `TBD`, `FIXME`, or `XXX` debt markers found in any Phase 25.4 modified file.

---

## Human Verification Required

None. All must-have truths are programmatically verifiable and the operator-driven smoke probe has been completed with a PASS verdict. No items require further human testing.

---

## Deferred Items

RESEARCH Open Q1 (worker-perch asymmetry — `spawn_worker_perch` unreliable for one-shot subagents) is explicitly carried forward per CONTEXT D-07 as a separate orthogonal investigation. This is **not a Phase 25.4 gap**: the Phase 25.4 goal is "no flat side-effect dirs" (the NEGATIVE invariant), which holds. Whether `spawn_worker_perch` fires reliably for one-shot subagents is a separate POSITIVE invariant question about subagent registration, not path composition. The smoke probe confirmed zero flat worker side-effects from the spool/inbox writers — the migration's objective.

---

## Gaps Summary

No gaps. All 14 must-have truths verified, all 16 requirement IDs satisfied, no unresolved debt markers, smoke probe PASS. Two in-checkpoint finishing fixes (`.more-done` gate predicate `48b61be` + Stop hook version-change early-return `72b4263`) were applied during the operator verification loop and are confirmed in the codebase.

---

_Verified: 2026-05-24_
_Verifier: Claude (gsd-verifier)_
