---
phase: quick-260513-v8f
plan: 01
subsystem: live-agent/wrapper
tags: [init-signoff, psyche-wrapper, orphan-detection, event-envelope, atomic-commits]

# Dependency graph
requires:
  - phase: aba13d9 (commit, Phase 27-02)
    provides: stream-default poll emission wraps deliveries in <EVENT type="msg"> envelope (cause of broken predicate)
  - phase: quick-260513-63n
    provides: --once flag on inner-poll subprocess (sets stage by making the loop drain self-fired INIT_SIGNOFFs within seconds instead of pulse-cadence minutes)
provides:
  - EVENT-envelope-tolerant INIT_SIGNOFF predicate at the wrapper's live inner-poll site
  - Functional orphan exit path: check_orphan returns still_gone, caller breaks loop on true
  - Single source of truth for INIT_SIGNOFF payload composition (compose_init_signoff_payload)
  - Structured <EVENT type="init_signoff" timestamp="..."> envelope shipped alongside legacy literal
affects: [live-agent shutdown, Phase 29 signoff-related cleanup, future programmatic INIT_SIGNOFF parsing]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Pure helper extraction for hermetic unit tests of subprocess/sleep-bound code (classify_orphan_return)"
    - "Compat-shaped envelope: keep legacy literal first line + append structured EVENT envelope (option-b from task spec)"
    - "Single payload-construction helper deduplicates 3 emit sites (signoff::run, signoff::signoff_result, wrapper::orphan)"

key-files:
  created: []
  modified:
    - src/live/wrapper/mod.rs - EVENT-aware predicate; orphan call site breaks on true; 5 new tests
    - src/live/wrapper/orphan.rs - classify_orphan_return helper; check_orphan returns still_gone; routes through compose_init_signoff_payload
    - src/live/signoff.rs - compose_init_signoff_payload helper; run + signoff_result deduplicated to call it; 3 new tests

key-decisions:
  - "Compat strategy option-b: keep legacy literal as FIRST LINE of payload so msg.contains(\"INIT_SIGNOFF\") predicate remains single-substring check (parity with lifecycle.rs:89); EVENT envelope appended on subsequent line"
  - "classify_orphan_return is currently a direct passthrough but isolated for unit testing — documents return-value contract and prevents future regression to hard-coded false"
  - "Orphan emit body text intentionally aligned with Self-initiated text (\"Signoff initiated by Self\") via dedup; per plan acceptance, timestamp source unchanged (chrono::Local) — full timestamp consistency is its own cleanup"
  - "check_orphan_caller_breaks_loop_on_true uses include_str! substring guard to encode the call-site shape (precedent: under_wrapper_env_name_matches_poll_contract)"
  - "Test count drift on cargo test --lib reflects test-binary partitioning, not test loss — all 8 new tests pass under --list verification"

patterns-established:
  - "Pure helper + substring guard: when subprocess-bound code resists direct unit testing, extract the decision-logic to a pure fn AND add an include_str! guard for the call-site shape (defense-in-depth contract test)"
  - "Payload helper as single source of truth: when 3+ sites construct a payload with mild variation, dedupe via compose_*_payload(timestamp, optional-body) helper exposing Option<&str> for variant body"

requirements-completed: [QUICK-260513-v8f]

# Metrics
duration: ~30min
completed: 2026-05-13
---

# Quick 260513-v8f: Fix endless INIT_SIGNOFF loop in psyche-wrapper — Summary

**Three-commit atomic fix wires INIT_SIGNOFF detection to survive the post-aba13d9 `<EVENT>` envelope AND wires the orphan-detection path to actually break the inner-poll loop; ships a structured `<EVENT type="init_signoff">` envelope alongside the legacy literal so the predicate stays a single substring check.**

## Performance

- **Duration:** ~30 min
- **Started:** 2026-05-13T22:00:00Z (approx)
- **Completed:** 2026-05-13T22:30:00Z (approx)
- **Tasks:** 3 atomic fix commits + 1 chore commit
- **Files modified:** 3 source files (mod.rs, orphan.rs, signoff.rs), 1 debug-doc move

## Accomplishments

- Wrapper inner-poll INIT_SIGNOFF detection restored under `<EVENT>` envelope (Commit A)
- Orphan path now actually breaks the inner-poll loop on confirmed orphan (Commit B)
- Three previous emit sites unified through `compose_init_signoff_payload` helper (Commit C)
- Structured `<EVENT type="init_signoff" timestamp="...">` envelope shipped, legacy literal preserved as first line for back-compat
- 8 new regression tests pinning predicate shape, orphan return-value contract, payload composition
- Debug session (`.planning/debug/psyche-wrapper-init-signoff-loop.md`) moved to `resolved/`

## Task Commits

Each task committed atomically; each commit independently revertable; `cargo build --release` and `cargo test` pass at HEAD of every commit.

1. **Commit A: EVENT-aware INIT_SIGNOFF predicate** — `ed21251` (fix)
   - `src/live/wrapper/mod.rs:187-205`: replace `line.trim_start().starts_with("INIT_SIGNOFF")` with `msg.contains("INIT_SIGNOFF")`
   - 3 new tests: `init_signoff_predicate_matches_event_envelope`, `init_signoff_predicate_matches_bare_line`, `init_signoff_predicate_does_not_match_unrelated`
2. **Commit B: Orphan return-value plumbing** — `b41c94e` (fix)
   - `src/live/wrapper/orphan.rs`: add `classify_orphan_return`; replace hard-coded `false` with `classify_orphan_return(still_gone)`
   - `src/live/wrapper/mod.rs:92-100`: wrap call as `if self.check_orphan() { self.log(...); break; }`
   - 2 new tests: `check_orphan_returns_true_when_self_perch_absent`, `check_orphan_caller_breaks_loop_on_true`
3. **Commit C: INIT_SIGNOFF emission envelope cleanup** — `f444066` (feat)
   - `src/live/signoff.rs`: add `compose_init_signoff_payload(timestamp, message)`; deduplicate `run` and `signoff_result`
   - `src/live/wrapper/orphan.rs:55`: route orphan-emit through the same helper
   - `src/live/wrapper/mod.rs`: add compat-strategy comment near predicate (no behavior change)
   - 3 new tests: `init_signoff_payload_contains_legacy_literal_with_commune`, `init_signoff_payload_contains_legacy_literal_no_commune`, `init_signoff_payload_predicate_still_matches`

**Plan metadata commit:** `c9db074` (chore: mark psyche-wrapper init-signoff-loop debug session resolved — moves debug file to `.planning/debug/resolved/`)

## Files Created/Modified

- `src/live/wrapper/mod.rs` — Predicate fix; orphan break wiring; 5 new tests (3 predicate + 2 orphan contract)
- `src/live/wrapper/orphan.rs` — `classify_orphan_return` helper; return-value plumbing; emit-site dedup through `compose_init_signoff_payload`
- `src/live/signoff.rs` — `compose_init_signoff_payload` helper; `run` and `signoff_result` deduplicated; 3 new tests
- `.planning/debug/resolved/psyche-wrapper-init-signoff-loop.md` — moved from `.planning/debug/`

## Decisions Made

- **Compat strategy (option-b):** Keep legacy `INIT_SIGNOFF (ts): ...` literal as first line of every payload variant so the wrapper predicate stays a single case-sensitive substring check. The new `<EVENT type="init_signoff">` envelope appends on a subsequent line, giving downstream consumers structured shape without breaking the wrapper detection path today. Rationale: parity with `lifecycle.rs:89`, single match expression, defense-in-depth.
- **Pure helper extraction for orphan return contract:** `check_orphan` itself is hard to unit-test (5s sleep, real perch dirs, real owlery). Extracted a pure passthrough `classify_orphan_return(still_gone) -> bool` so the return-value contract is unit-testable; the test guards against future regression to hard-coded `false`.
- **Include_str! contract guard:** Encoded the mod.rs call-site shape `if self.check_orphan() { ... break; }` via an `include_str!("mod.rs")` substring assertion. Precedent: `under_wrapper_env_name_matches_poll_contract`.
- **Orphan body text aligned with Self-initiated:** Routed orphan-emit through `compose_init_signoff_payload(&timestamp, None)`. Net effect: orphan-fired INIT_SIGNOFF body text changes from `"Self parent session disappeared, autonomous shutdown"` to `"Signoff initiated by Self"`. Per plan acceptance criteria (Commit C action step 2), full timestamp/text consistency between Self-initiated and orphan-initiated emit is a known follow-up.
- **No predicate change in Commit C:** The legacy first line is preserved, so `msg.contains("INIT_SIGNOFF")` still matches. Only added a clarifying comment near the predicate documenting the compat strategy.

## Deviations from Plan

None — plan executed exactly as written.

The plan's verification requirements were fully met:
- 3 atomic, independently-revertable commits (A → B → C) plus 1 chore commit (D)
- `cargo build --release` succeeds at HEAD of every commit
- `cargo test -- --test-threads=1` whole-suite passes at HEAD of every commit (note: parallel test runs without `--test-threads=1` exhibit pre-existing SPT_HOME contention failures unrelated to this work — STATE.md Phase 18.7 P01 documents this race class)
- 8 new regression tests added across 3 commits, all passing
- Grep confirmed no other consumer parses INIT_SIGNOFF body — only emit/predicate/log/comment sites
- Grep confirmed no remaining `format!("INIT_SIGNOFF ...")` calls outside the helper
- No golden-test fixtures touch INIT_SIGNOFF — no regen required
- No `psyche.md` change — no `include_str!` cache-bust beyond normal release rebuild

## Issues Encountered

None during execution. Pre-existing parallel-test contention (SPT_HOME global env var races between `cleanup`, `hook_idle`, `owlery`, `signoff`, `resume`, `context`, `commune`, `pick_spec` test modules) was observed when running `cargo test` without `--test-threads=1`; this is a known issue documented in STATE.md and is not regressed by this work — same test set passes serially.

## Manual Verification (Pending)

End-to-end verification per plan's `<verification>` block requires a deploy + live-agent smoke test:

```
powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch
# Spawn live agent, run `live signoff <id>` with a tiny commune body
# Tail the psyche log; expect a SINGLE occurrence of
#   "INIT_SIGNOFF detected, firing final context save"
# followed by `[PSYCHE] final (exit=...)`, then `Final psyche invocation
# complete, wrapper exiting`. Wrapper process exits. No second
# "INIT_SIGNOFF detected" line. No "poll iteration N+1 starting" after
# the final-session log block. `Grep INIT_SIGNOFF <agent>.log` returns
# ≤ 2 occurrences (one emit, one detection).
```

This deploy step is intentionally deferred per project convention (DEPLOY.ps1 is user-driven; CLAUDE.md `## Build & Deploy` stipulates the executor stops before `/reload-plugins`).

## User Setup Required

None — no external service configuration required. Manual verification step is a local-only deploy + smoke test, no third-party credentials.

## Next Phase Readiness

- Wrapper has TWO independent exit valves for signoff/orphan (predicate AND orphan return). A future regression in either path still terminates the loop.
- Single helper `compose_init_signoff_payload` is the documented anchor for any Phase 29-aligned envelope-cleanup work. Adding new payload variants (e.g. with `from=` attribution) means modifying ONE function, not three.
- Compat strategy means downstream code can begin parsing `<EVENT type="init_signoff">` without coordinated emitter/detector flip-day — emitters already ship both shapes.

**Follow-up candidates (out of scope for this quick task):**
- Align orphan-emit timestamp source with `crate::common::time::format_timestamp()` for full consistency between Self-initiated and orphan-initiated INIT_SIGNOFF
- Stricter predicate that parses the EVENT envelope and keys exclusively on `type="init_signoff"` (eliminates T-v8f-01 false-positive risk from user-typed quotes containing `INIT_SIGNOFF`)
- Decide whether the legacy first-line literal should ever be dropped (gated on confidence that no spool-resident message predates the new emission); on drop, the predicate must widen to also match `type="init_signoff"`

---

## Self-Check: PASSED

**Files verified to exist:**
- `src/live/wrapper/mod.rs` — FOUND (modified)
- `src/live/wrapper/orphan.rs` — FOUND (modified)
- `src/live/signoff.rs` — FOUND (modified)
- `.planning/debug/resolved/psyche-wrapper-init-signoff-loop.md` — FOUND (moved)

**Commits verified to exist:**
- `ed21251` — FOUND (fix(v8f): EVENT-aware INIT_SIGNOFF predicate in wrapper inner-poll)
- `b41c94e` — FOUND (fix(v8f): wire orphan return value to wrapper inner-poll break)
- `f444066` — FOUND (feat(v8f): structured INIT_SIGNOFF envelope with legacy token preserved)
- `c9db074` — FOUND (chore(v8f): mark psyche-wrapper init-signoff-loop debug session resolved)

**Tests verified:**
- 8 new tests present in `cargo test --lib -- --list` and all pass:
  - `live::wrapper::tests::init_signoff_predicate_matches_event_envelope`
  - `live::wrapper::tests::init_signoff_predicate_matches_bare_line`
  - `live::wrapper::tests::init_signoff_predicate_does_not_match_unrelated`
  - `live::wrapper::tests::check_orphan_returns_true_when_self_perch_absent`
  - `live::wrapper::tests::check_orphan_caller_breaks_loop_on_true`
  - `live::signoff::tests::init_signoff_payload_contains_legacy_literal_with_commune`
  - `live::signoff::tests::init_signoff_payload_contains_legacy_literal_no_commune`
  - `live::signoff::tests::init_signoff_payload_predicate_still_matches`

---
*Phase: quick-260513-v8f*
*Completed: 2026-05-13*
