---
phase: 37-waker-hardening
plan: 01
type: execute
wave: 1
depends_on: ["35-01", "36-01"]
files_modified:
  - plugins/autonomous-loop/scripts/waker.sh
  - plugins/autonomous-loop/scripts/launchd-lib.sh
  - plugins/autonomous-loop/tests/test-spawn-invariant.sh
  - plugins/autonomous-loop/tests/test-plist-collision.sh
autonomous: true
requirements:
  - WAKE-01
  - WAKE-02
  - WAKE-03
  - WAKE-04
  - WAKE-05

must_haves:
  truths:
    - "WAKE-01: spawn_claude_resume validates session_id matches UUID regex; refuses spawn on mismatch with spawn_refused_invalid_session_id provenance event"
    - "WAKE-02: spawn cd's to dirname(contract_path), not dirname(state_dir); fixes cwd-on-resume cross-contamination"
    - "WAKE-03: generate_plist detects launchd Label collision (launchctl list | grep -c <label> >= 1); archives existing plist to state_dir/orphans/<unixts>/, unloads, regenerates"
    - "WAKE-04: Five-check pre-spawn invariant gates every spawn: (a) UUID, (b) heartbeat-from-cwd, (c) bound_cwd matches contract_dir, (d) launchd label uniqueness, (e) no concurrent reclaim"
    - "WAKE-05: Every spawn refusal emits typed provenance event AND user notification; no silent drops"
    - "shellcheck clean; bun scripts/validate-plugins.mjs PASS; Phases 35 + 36 tests still pass"
  artifacts:
    - path: "plugins/autonomous-loop/scripts/waker.sh"
      provides: "5-check invariant gating spawn_claude_resume"
      contains: "spawn_refused"
    - path: "plugins/autonomous-loop/scripts/launchd-lib.sh"
      provides: "Label collision detection in generate_plist"
      contains: "label_collision"
---

<objective>
Make `claude --resume` spawn structurally impossible unless five invariants hold. Fix the cwd-on-resume bug. Detect and resolve launchd Label collisions in generate_plist.

Purpose: Refuse-by-default. Worst spawn outcome becomes "didn't auto-resume" (user notices via doctor). Never "resumed wrong session" (silent JSONL contamination).
</objective>

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

<context>
@.planning/phases/37-waker-hardening/37-CONTEXT.md
@plugins/autonomous-loop/CLAUDE.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Harden spawn_claude_resume in waker.sh</name>
  <files>plugins/autonomous-loop/scripts/waker.sh</files>
  <read_first>
    - plugins/autonomous-loop/scripts/waker.sh
    - plugins/autonomous-loop/scripts/provenance-lib.sh
  </read_first>
  <action>
Edit `spawn_claude_resume()`:
1. Source provenance-lib.sh at top of waker.sh; set `_PROV_AGENT="waker.sh"`.
2. Before any spawn action, run the 5-check invariant in order. On any failure, emit `spawn_refused_<which>` provenance event + emit_notification, return 0.
   - (a) UUID validity: `[[ "$session_id" =~ ^[0-9a-f-]{36}$ ]]` — refuse if `pending-bind`, `unknown`, empty, etc.
   - (b) Heartbeat-from-cwd: read heartbeat.json; require `bound_cwd != ""` AND non-zero `last_wake_us`.
   - (c) bound_cwd matches: `bound_cwd == dirname(contract_path)`.
   - (d) Label uniqueness: `launchctl list | awk '{print $3}' | grep -cFx "$label"` == 1 (or 0 if not loaded).
   - (e) No concurrent reclaim: re-read registry generation under lock; require it equals the value seen before invariant check.
3. cwd: change `cwd=$(dirname "$state_dir")` to `cwd=$(dirname "$contract_path")`. The contract_path comes from `read_registry_entry`.
4. All refusal paths emit provenance with reason= and call emit_notification (existing notifications-lib).
  </action>
</task>

<task type="auto">
  <name>Task 2: Add label collision detection to generate_plist</name>
  <files>plugins/autonomous-loop/scripts/launchd-lib.sh</files>
  <read_first>
    - plugins/autonomous-loop/scripts/launchd-lib.sh
  </read_first>
  <action>
Before writing the plist file, check whether `launchctl list` already shows a job with this Label:
```
existing_count=$(launchctl list 2>/dev/null | awk '{print $3}' | grep -cFx "$label" || echo 0)
```
If `existing_count >= 1`:
1. `launchctl bootout gui/$(id -u)/$label 2>/dev/null || true`
2. If `~/Library/LaunchAgents/$label.plist` exists: move it to `$state_dir/orphans/$(date +%s)/$label.plist`.
3. Emit a `label_collision_resolved` provenance event (source provenance-lib.sh in launchd-lib.sh).
4. Continue with normal plist generation.

This makes `generate_plist` self-healing: it can never silently overwrite a loaded plist, and the orphan is preserved for forensics.
</action>
</task>

<task type="auto">
  <name>Task 3: tests/test-spawn-invariant.sh (5 refusal cases + 1 happy path)</name>
  <files>plugins/autonomous-loop/tests/test-spawn-invariant.sh</files>
  <read_first>
    - plugins/autonomous-loop/scripts/waker.sh
    - plugins/autonomous-loop/tests/test-session-bind.sh
  </read_first>
  <action>
Six test cases stubbing the registry and heartbeat with each scenario:
1. Invalid UUID (session_id="pending-bind") → spawn_refused_invalid_session_id event; no `claude` process spawned (verify via NOOP_CLAUDE env shim).
2. No heartbeat → spawn_refused_no_heartbeat.
3. cwd-drift detected (bound_cwd != contract_dir) → spawn_refused_cwd_drift.
4. Mock launchctl shim returning duplicate label → spawn_refused_label_collision.
5. Generation mismatch (registry generation incremented after invariant snapshot) → spawn_refused_generation_drift.
6. All invariants hold → spawn_succeeded provenance event; mocked claude was invoked with correct cwd (=dirname contract_path).

Use `PATH` shim with a fake `claude` script that records its argv to a file; assert `cwd` and `--resume <id>` from that file.
</action>
</task>

<task type="auto">
  <name>Task 4: tests/test-plist-collision.sh (3 cases)</name>
  <files>plugins/autonomous-loop/tests/test-plist-collision.sh</files>
  <read_first>
    - plugins/autonomous-loop/scripts/launchd-lib.sh
  </read_first>
  <action>
Three cases with `launchctl` shim:
1. No existing plist → generate_plist writes cleanly; no orphan dir.
2. Existing UNLOADED plist → archived to orphans/<ts>/, regenerated; label_collision_resolved provenance emitted.
3. Existing LOADED plist (shim says count=1) → bootout invoked, plist archived, regenerated.

Override `launchctl` via PATH shim that reads from a file describing what it should report.
</action>
</task>

<task type="auto">
  <name>Task 5: shellcheck + tests + commit</name>
  <action>
1. `chmod +x` new tests.
2. shellcheck on waker.sh, launchd-lib.sh, both new tests — exit 0.
3. Run new tests — both exit 0.
4. Run regression: test-provenance.sh + test-session-bind.sh + test-heartbeat-stdin.sh — all green.
5. `bun scripts/validate-plugins.mjs` — PASS.
6. Commit:
```
feat(autonomous-loop): waker hardening + launchd collision defense (WAKE-01..05)

5-check pre-spawn invariant in waker.sh:

- UUID validity (refuses pending-bind/unknown/empty)
- Heartbeat-from-cwd (proof of life from inside contract dir)
- bound_cwd == dirname(contract_path) (no cwd drift)
- Launchd Label uniqueness (no collision)
- Generation-drift detection under registry lock

cd to dirname(contract_path), not dirname(state_dir) — fixes the
cwd-on-resume cross-contamination root cause.

generate_plist: detects launchd Label collisions, archives orphan
plists to state_dir/orphans/<ts>/, unloads, regenerates. No silent
overwrites of loaded plists ever.

Every refusal emits typed provenance event + user-facing notification.
Refuse-by-default: worst outcome is "didn't auto-resume", never
"resumed wrong session".

Refs: WAKE-01..05

```
  </action>
</task>

</tasks>
```
