---
phase: quick-260520-mth
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/live/wrapper/orphan.rs
  - .planning/debug/phantom-init-signoff-after-handoff.md
  - .planning/debug/resolved/phantom-init-signoff-after-handoff.md
autonomous: true
requirements:
  - QUICK-260520-mth-PHANTOM-INIT-SIGNOFF

must_haves:
  truths:
    - "When Self transiently appears gone but recovers within the 5s grace window, NO INIT_SIGNOFF envelope is delivered to the psyche spool."
    - "When Self is genuinely gone (still_gone == true after grace), INIT_SIGNOFF is delivered exactly once, as before."
    - "The synchronous `_echo-commune --force` orphan-path fire still runs BEFORE the grace sleep (semantics unchanged — Self did work prior to now)."
    - "Existing orphan-path tests (Test A through Test H) still pass (with Test A's source-order assertion still satisfied: build_orphan_fire_args call precedes compose_init_signoff_payload)."
    - "`cargo build --release` succeeds and `cargo test` passes."
    - "The debug ticket is moved to `.planning/debug/resolved/` with a resolution note appended (commit hash + brief description)."
  artifacts:
    - path: "src/live/wrapper/orphan.rs"
      provides: "Reordered check_orphan: grace sleep + still_gone recheck BEFORE compose/deliver of INIT_SIGNOFF."
      contains: "thread::sleep"
    - path: ".planning/debug/resolved/phantom-init-signoff-after-handoff.md"
      provides: "Moved debug ticket with resolution note (commit hash + brief description)."
      contains: "## Resolution Note"
  key_links:
    - from: "src/live/wrapper/orphan.rs check_orphan"
      to: "compose_init_signoff_payload / send::deliver_body_anonymous"
      via: "conditional delivery gated on still_gone == true AFTER grace sleep"
      pattern: "if still_gone"
    - from: "src/live/wrapper/orphan.rs check_orphan source order"
      to: "Test A invariant (build_orphan_fire_args call precedes compose_init_signoff_payload)"
      via: "echo-commune fire stays in current position (before grace), compose/deliver moves AFTER grace"
      pattern: "build_orphan_fire_args(&uuid,"
---

<objective>
Fix phantom INIT_SIGNOFF race in `check_orphan()` where the wrapper delivers an INIT_SIGNOFF envelope to its own psyche spool BEFORE the 5-second grace-period recheck. When Self recovers during the grace window, the already-delivered envelope sits in the spool and the very next `poll_psyche()` drain in the same iteration fires `final_session()`, exiting the wrapper despite Self being alive.

Purpose: Restore the invariant the grace period exists to enforce — no irreversible action until liveness is confirmed.

Output: One-block reorder in `src/live/wrapper/orphan.rs`, a unit test that pins the new ordering invariant (grace-recovery → zero envelopes in spool), and the debug ticket moved to `.planning/debug/resolved/` with a resolution note.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
</execution_context>

<context>
@.planning/debug/phantom-init-signoff-after-handoff.md

@src/live/wrapper/orphan.rs

<interfaces>
<!-- Key call sites and source-order invariants the executor needs.
     Extracted from src/live/wrapper/orphan.rs read during planning. -->

Current order of operations in `check_orphan()` (orphan.rs:141-244):
1. `self_alive` probe (line 147-161). If alive, `return false` (line 163-165). KEEP.
2. `read_self_session_uuid_from_info` + synchronous `_echo-commune --force` blocking fire (line 175-199). KEEP — semantics unchanged.
3. Log line "Self parent session is gone (orphan detected via parent_pid), triggering INIT_SIGNOFF" (line 201). REWORD to reflect new ordering (e.g. "entering grace period before INIT_SIGNOFF").
4. Compose timestamp + stamp + `compose_init_signoff_payload` (line 206-211). MOVE after grace+recheck.
5. `if owlery::ready_file(&self.psyche_id).exists() { ... deliver_body_anonymous ... }` (line 212-217). MOVE after grace+recheck.
6. `std::thread::sleep(5s)` grace (line 219). MOVE to immediately follow the echo-commune fire (step 2).
7. `still_gone` recheck (line 221-231). MOVE up — runs right after the grace sleep.
8. Log "confirmed gone" / "recovered" branch (line 232-236). KEEP both messages; gate the compose+deliver on `still_gone == true`.
9. `classify_orphan_return(still_gone)` (line 243). KEEP — return value semantics unchanged.

New target order:
- self_alive probe (unchanged)
- echo-commune --force fire (unchanged position)
- log "entering grace period before INIT_SIGNOFF"
- thread::sleep(5s) grace
- still_gone recheck
- if !still_gone: log "recovered, continuing normally" + return classify_orphan_return(false)
- log "confirmed gone after grace period, delivering INIT_SIGNOFF"
- compose timestamp + stamp + compose_init_signoff_payload
- if owlery::ready_file(&self.psyche_id).exists() { deliver_body_anonymous }
- return classify_orphan_return(true)

Test A invariant (orphan_fire_tests::test_a_sync_fire_argv_appears_before_compose_init_signoff):
- Currently asserts `build_orphan_fire_args(&uuid,` call position < `compose_init_signoff_payload(` position in source.
- After the fix this remains TRUE — the echo-commune call site stays where it is; compose moves further down.
- No change to Test A required.

Test B invariant (test_b_sync_fire_argv_appears_after_if_self_alive_guard):
- Currently asserts `if self_alive {` position < `build_orphan_fire_args(&uuid,` position.
- Unchanged by the fix.

Out-of-scope files (DO NOT EDIT — verified by debug ticket):
- src/live/wrapper/mod.rs (poll predicate `is_init_signoff_envelope`, `drain_stale_init_signoffs` — both correct as written)
- src/live/wrapper/lifecycle.rs
- src/live/signoff.rs (`compose_init_signoff_payload` is the shared helper — keep using it)
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Reorder check_orphan grace recheck before INIT_SIGNOFF delivery (+ unit test)</name>
  <files>src/live/wrapper/orphan.rs</files>
  <behavior>
    New unit test `test_grace_recovery_does_not_emit_init_signoff_in_source_order` in `orphan_fire_tests`:
    - Use `include_str!("orphan.rs")` to read the production region (bytes BEFORE the `mod orphan_fire_tests` marker, mirroring Test E2's pattern).
    - Locate the byte index of `std::thread::sleep(std::time::Duration::from_secs(5))` inside `check_orphan`.
    - Locate the byte index of `compose_init_signoff_payload(` inside `check_orphan`.
    - Locate the byte index of `deliver_body_anonymous(` inside `check_orphan`.
    - Assert sleep_pos < compose_pos AND sleep_pos < deliver_pos. Without the fix these assertions fail (current order: deliver_pos < sleep_pos).
    Additional sanity test `test_still_gone_recheck_precedes_init_signoff_delivery_in_source_order`:
    - Locate byte index of `let still_gone =` (the recheck binding) and assert it appears BEFORE `compose_init_signoff_payload(` and BEFORE `deliver_body_anonymous(`.
    Existing Test A (build_orphan_fire_args call before compose) MUST still pass — the echo-commune fire stays in its current position; only the compose/deliver block moves further down.
  </behavior>
  <action>
    Reorder the body of `check_orphan` in `src/live/wrapper/orphan.rs` so that the 5-second grace sleep + `still_gone` recheck run BEFORE the INIT_SIGNOFF compose/deliver block. Concrete edits:

    1. After the existing synchronous `_echo-commune --force` block (the `match read_self_session_uuid_from_info(&self.self_id) { ... }` arm ending around line 199), replace the current log+compose+deliver+sleep+recheck+log sequence (current lines 201-236) with this new order:

       a. Log: `self.log("Self parent session is gone (orphan detected via parent_pid), entering grace period before INIT_SIGNOFF");`
       b. `std::thread::sleep(std::time::Duration::from_secs(5));`
       c. Compute `still_gone` using the IDENTICAL closure/expression already at current lines 221-231 (do NOT change the liveness probe logic — parent_pid then pid fallback).
       d. `if !still_gone { self.log("Self parent recovered during grace period, continuing normally"); return classify_orphan_return(false); }`
       e. Log: `self.log("Self parent confirmed gone after grace period, delivering INIT_SIGNOFF");`
       f. Compose timestamp + stamp + `init_signoff` payload using the SAME calls as current lines 206-211 (chrono::Local::now formatted, `crate::common::git::stamp()`, `crate::live::signoff::compose_init_signoff_payload(&timestamp, &stamp, None)`).
       g. `if owlery::ready_file(&self.psyche_id).exists() { let psyche_id = self.psyche_id.clone(); let _ = std::panic::catch_unwind(move || { send::deliver_body_anonymous(&psyche_id, &init_signoff); }); }` — IDENTICAL to current lines 212-217.
       h. `classify_orphan_return(true)` as the final expression of this branch.

    2. The `self_alive` early-return at the top (lines 147-165) and the echo-commune fire block (lines 167-199) MUST remain exactly where they are — do not touch them. The fire stays semantically "Self did work prior to now" and is independent of the grace decision (per debug ticket Resolution).

    3. Add the two unit tests described in `<behavior>` to the existing `orphan_fire_tests` module at the end of the file. Use the `include_str!("orphan.rs")` + `src.find(...)` byte-index pattern already established by Test A, Test B, and Test E2. Scope the search to the production region (bytes before `mod orphan_fire_tests`) where appropriate.

    4. Do NOT modify any other file. No mod.rs, no lifecycle.rs, no signoff.rs edits.

    5. After editing, run `cargo build --release` and `cargo test` to confirm.

    6. Commit message: `fix(quick-260520-mth): defer INIT_SIGNOFF delivery until after grace recheck` with a body referencing `.planning/debug/phantom-init-signoff-after-handoff.md` as the diagnosis source.
  </action>
  <verify>
    <automated>cargo build --release 2>&amp;1 | tail -5 ; cargo test --lib live::wrapper::orphan -- --test-threads=1 2>&amp;1 | tail -30</automated>
  </verify>
  <done>
    - `src/live/wrapper/orphan.rs::check_orphan` has thread::sleep(5s) + still_gone recheck BEFORE the compose/deliver block.
    - New tests `test_grace_recovery_does_not_emit_init_signoff_in_source_order` and `test_still_gone_recheck_precedes_init_signoff_delivery_in_source_order` exist and pass.
    - Existing Tests A through H still pass.
    - `cargo build --release` succeeds.
    - `cargo test` (full suite) passes.
    - One atomic commit on `main` with the fix + tests.
  </done>
</task>

<task type="auto">
  <name>Task 2: Move debug ticket to resolved/ with resolution note</name>
  <files>.planning/debug/phantom-init-signoff-after-handoff.md, .planning/debug/resolved/phantom-init-signoff-after-handoff.md</files>
  <action>
    Move `.planning/debug/phantom-init-signoff-after-handoff.md` to `.planning/debug/resolved/phantom-init-signoff-after-handoff.md` (via `git mv` so the rename is tracked). Append a new `## Resolution Note` section at the end of the moved file containing:

    - The Task 1 commit hash (get it from `git log -1 --format=%H` after Task 1 commits).
    - A 1-2 sentence description: "Fixed by reordering `check_orphan` in `src/live/wrapper/orphan.rs` so the 5-second grace sleep and `still_gone` recheck run BEFORE the INIT_SIGNOFF compose/deliver block. The synchronous `_echo-commune --force` orphan-path fire stays in its original position. Grace-period recovery now correctly suppresses the envelope from ever reaching the psyche spool."
    - Date: 2026-05-20.

    Commit message: `docs(quick-260520-mth): mark phantom-init-signoff-after-handoff resolved`. Use a separate commit from Task 1 (per the "Atomic commits per task" constraint).

    Do NOT modify the original diagnosis content — only append the Resolution Note section at the end.
  </action>
  <verify>
    <automated>test -f .planning/debug/resolved/phantom-init-signoff-after-handoff.md &amp;&amp; ! test -f .planning/debug/phantom-init-signoff-after-handoff.md &amp;&amp; grep -q "## Resolution Note" .planning/debug/resolved/phantom-init-signoff-after-handoff.md &amp;&amp; echo OK</automated>
  </verify>
  <done>
    - `.planning/debug/phantom-init-signoff-after-handoff.md` no longer exists.
    - `.planning/debug/resolved/phantom-init-signoff-after-handoff.md` exists with original content + appended `## Resolution Note` section containing the Task 1 commit hash, brief description, and date.
    - One atomic commit on `main` for the move + note.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| wrapper subprocess → own psyche spool | Wrapper writes envelopes to its own psyche's SQLite spool; envelopes are then drained and acted on by the same wrapper's inner-poll subprocess. A speculative write that is later "un-decided" creates a phantom signal. This fix tightens that boundary: writes only occur on confirmed orphan. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-q260520mth-01 | Tampering (self-tampering via speculative state) | `check_orphan` in `src/live/wrapper/orphan.rs` | mitigate | Reorder so INIT_SIGNOFF envelope is only delivered when `still_gone == true` after grace recheck. New source-order unit test pins the invariant. |
| T-q260520mth-02 | Denial of Service (false-positive wrapper exit while Self is alive) | `check_orphan` + inner-poll loop in `src/live/wrapper/mod.rs` | mitigate | Same fix as T-q260520mth-01 — without the speculative envelope, the inner-poll loop has nothing to match in `is_init_signoff_envelope`, no `final_session` fires, wrapper continues running. |
| T-q260520mth-03 | Repudiation (genuine signoff swallowed by overly aggressive grace-recovery retraction) | Alternative "drain on recovery" fix path | accept (rejected alternative) | Per debug ticket Resolution.fix: rejected the drain-on-recovery alternative as racy. Our chosen fix (defer delivery) cannot accidentally discard a legitimate concurrent signoff because nothing is delivered to discard. |
| T-q260520mth-SC | Tampering | npm/pip/cargo installs | accept | No new package installs — pure source reorder + new unit tests in existing module. No package legitimacy gate needed. |
</threat_model>

<verification>
- `cargo build --release` succeeds.
- `cargo test` passes (full suite, not just orphan).
- `grep -n "thread::sleep" src/live/wrapper/orphan.rs` shows the sleep BEFORE the line containing `compose_init_signoff_payload(`.
- New unit tests `test_grace_recovery_does_not_emit_init_signoff_in_source_order` and `test_still_gone_recheck_precedes_init_signoff_delivery_in_source_order` are present and pass.
- `git log --oneline -3` shows two new commits: the fix (Task 1) and the resolved-doc move (Task 2).
- `.planning/debug/resolved/phantom-init-signoff-after-handoff.md` exists and contains `## Resolution Note`.

Deferred verification (NOT in this plan):
- Live integration repro of "handoff → transient Self-gone → grace-recovers → no phantom signoff" — would require staging a binary handoff with a probe-failure injection point. Source-order unit tests + the existing orphan_fire_tests suite are deemed sufficient evidence per constraints. User runs `DEPLOY.ps1` manually post-merge and can observe absence of the phantom-signoff log pattern on the next handoff.
</verification>

<success_criteria>
- `src/live/wrapper/orphan.rs::check_orphan` delivers INIT_SIGNOFF ONLY when `still_gone == true` after the 5s grace recheck.
- The echo-commune `--force` orphan-path fire still runs synchronously BEFORE the grace sleep (Test A and Test B invariants preserved).
- Two new source-order unit tests pin the new invariant (sleep before compose/deliver; still_gone recheck before compose/deliver).
- `cargo build --release` and `cargo test` both pass.
- Debug ticket moved to `.planning/debug/resolved/` with `## Resolution Note` appended (commit hash + brief description + date).
- Two atomic commits on `main`: the fix + tests (Task 1), and the doc move (Task 2).
</success_criteria>

<output>
Create `.planning/quick/260520-mth-fix-phantom-init-signoff-race/260520-mth-SUMMARY.md` when done.
</output>
