# Autonomy P2b — Resolver + Watchdog Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Codify the babysitter: resolution policy engine (A2) as library+CLI, watchdog (A4/I5/I6), unattended-landing guardrails (V1 protected paths + review-proof), artifacts gate (X8), canary ordering (X3), review diversity (X7), flaky detection (W8), failure snapshots (W6).

**Architecture:** `src/resolver.js` is a LIBRARY with a thin CLI verb (`runplan resolve <slug>`) — the P3 daemon later calls the same function (`spec/IMPLEMENTER-NOTES.md` § Transition seam). Canonical spec: `spec/RESOLVER.md`. Gates extend `src/engine/gates.js`; land guardrails extend `src/engine/ship.js` (both exist after P2a).

**Tech Stack:** Node, existing taxonomy + presets.

**Sequencing:** After `autonomy-p2a-state-core` lands. Handoff plan P2 second half + P2 deltas (X3/X7/X8, W6/W8, V1). Read `spec/RESOLVER.md` END TO END first.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1 | src/resolver.js | single task |
| 2 | Task 2, Task 3 | src/resolver.js · bin/runplan, src/runner.js | ✅ no overlap |
| 3 | Task 4, Task 5 | src/engine/watchdog.js, src/runner.js · src/engine/scheduler.js | ✅ no overlap |
| 4 | Task 6, Task 7 | src/engine/gates.js · src/engine/ship.js | ✅ no overlap |
| 5 | Task 8, Task 9 | src/resolver.js, src/fail-taxonomy.js, src/engine/repair.js · lib/risk-router.sh, src/engine/lease.js | ✅ no overlap |
| 6 | Task 10 | test/chaos/scenarios/ | single task |

---

### Task 1: Resolver deterministic tier

**Wave:** 1 · **Blocks:** Task 2, Task 3 · **Blocked by:** —

**Files:**
- Create: `src/resolver.js` + colocated `src/resolver.test.js`.

**Contract:** `spec/RESOLVER.md` is canonical: closed action set, full policy table (every fail-class × attempt), breakers (max resolutions per run/class, backoff), failure input contract (structured stderr JSON, `classifyFailure` fallback — handle BOTH), `resolve.decision`/`resolve.outcome {refDecisionSeq, action, success}` journal kinds, env-repair registry (`env-repair:<registry-id>` form). Deterministic tier ONLY here — LLM rung is Task 2. Signature: `resolve(journalPath, context) -> {action, detail}` pure decision; separate `execute(decision)`.

**Behavior:** Table-covered classes byte-identical deterministic (regression-tested). Run-fatal shortlist → escalate immediately. NEVER weakens gates — `.warnignore`/review-skip not in action set by construction.

**Acceptance:**
- Run: `node --test src/resolver.test.js`
- Expected: PASS — every policy-table row exercised from fixture journals

- [ ] Tests FROM spec table first · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 2: Resolver LLM rung

**Wave:** 2 · **Blocks:** Task 8 · **Blocked by:** Task 1

**Files:**
- Modify: `src/resolver.js`.

**Contract:** `spec/RESOLVER.md` § LLM rung: exact prompt + response schema, invoked ONLY for table rows delegating to it, schema-validated response, ANY failure (timeout, invalid JSON, unavailable) → fail-closed to `escalate`, never a guess. LLM-disabled mode → deterministic behavior identical to Task 1 (regression gate).

**Acceptance:**
- Run: `node --test src/resolver.test.js` (LLM-stub cases)
- Expected: PASS — invalid LLM response → escalate; disabled-LLM byte-identical to table

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 3: `runplan resolve <slug>` CLI verb

**Wave:** 2 · **Blocks:** Task 4 · **Blocked by:** Task 1

**Files:**
- Modify: `bin/runplan`, `src/runner.js` — verb wiring only (logic stays in the library).

**Contract:** `runplan resolve <slug> [--dry-run]` — reads journal + last exit artifacts, computes ONE action, executes (or prints on dry-run), exits. Per `spec/IMPLEMENTER-NOTES.md` § Transition seam: babysit-runplan skill delegates to this immediately (partial autonomy before the daemon exists).

**Acceptance:**
- Run: chaos gate-red scenario, then `runplan resolve <fixture-slug> --dry-run`
- Expected: prints the policy-table action for the journaled failure, exit 0, no side effects on dry-run

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 4: Watchdog (A4, I5, I6 task-level)

**Wave:** 3 · **Blocks:** Task 10 · **Blocked by:** Task 3

**Files:**
- Create: `src/engine/watchdog.js`; Modify: `src/runner.js` — in-run enforcement while the runner owns the run (daemon tick adopts it in P3).

**Contract (handoff plan A4 + PART V I5/I6):**
- Per-state time budgets (config via `resolveConfig`) + agent idle timeout; breach → SIGTERM pgid → 10s grace → SIGKILL → classify `task-stalled`/`agent-idle`.
- Identity check BEFORE any signal: pid + start-time match liveness sidecar (I5) — mismatch = already dead, crash path, NEVER signal a recycled pid.
- Agents spawn via `setsid` into own process group; liveness records pgid (P2a liveness module carries the fields).
- Output-size cap on captured logs (truncate + journal, no OOM).

**Acceptance:**
- Run: chaos hung-agent + orphan-forker scenarios
- Expected: hung agent reaped within budget, orphans dead after kill (pgid), classified correctly; pid-reuse fixture never signaled

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 5: X3 canary-first dispatch order

**Wave:** 3 · **Blocks:** — · **Blocked by:** —

**Files:**
- Modify: `src/engine/scheduler.js` — pure ordering function.

**Contract (handoff plan X3):** Within a ready set: risk tier DESC, estimated cost ASC (static tier table until V4 stats). Pure function, unit-tested, no config.

**Acceptance:**
- Run: scheduler unit tests
- Expected: fixture ready-set orders as specified; chaos gate-red task dispatches first in its wave

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 6: X8 artifacts gate + V1 protected-path gate

**Wave:** 4 · **Blocks:** Task 10 · **Blocked by:** Task 4

**Files:**
- Modify: `src/engine/gates.js`.

**Contract:**
- X8: optional task fields `artifacts: [glob...]`, `verifycmd`; post-implement (pre-review): every glob matches ≥1 changed/added file on task branch; `verifycmd` exit 0 in task worktree; failure → `artifacts-missing` (fixer ladder).
- V1 protected paths: runconfig `protectedPaths` + non-removable compiled defaults (`.claude/`, `hooks/`, `.github/`, `**/ship.sh`, `spec/`, engine `src/` when target repo IS this harness, `.warnignore`); `git diff --name-only <integration>..<task-branch>` ∩ protected → `protected-path-touched` → quarantine + escalate, NOT fixer-repairable. Per-task whitelist ONLY via explicit `gated` decision resolved at preflight (division of labor per `spec/RESOLVER.md`: runner quarantines, resolver row = escalate).

**Acceptance:**
- Run: chaos exits-0-no-commit scenario + protected-path-touch fixture
- Expected: former fails at artifacts gate before review; latter quarantines with `protected-path-touched`, fixer never invoked

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 7: V1 review-proof land gate + `land-failed` resolver path

**Wave:** 4 · **Blocks:** Task 10 · **Blocked by:** Task 4

**Files:**
- Modify: `src/engine/ship.js`.

**Contract:** Land runs ONLY after journal (not memory) shows `review.passed` for every landed task, both stages (P1 writes the records) — violation → `land-blocked-unreviewed`. `ship.sh land` non-zero → `land-failed` → resolver (never a retry loop around partially-landed state; I4 intent from P2a already wraps it).

**Acceptance:**
- Run: chaos unreviewed-branch scenario
- Expected: land refuses with `land-blocked-unreviewed`; injected land failure classifies `land-failed` and reaches resolver

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 8: W6 failure snapshots + W8 flaky-gate rule

**Wave:** 5 · **Blocks:** Task 10 · **Blocked by:** Task 2, Task 6

**Files:**
- Modify: `src/engine/repair.js` (snapshot on quarantine), `src/fail-taxonomy.js` (flaky evidence field), `src/resolver.js` (flaky rule).

**Contract (handoff plan W6/W8):** Quarantine writes snapshot bundle (diff, gate output, journal tail, wrapper events) under `runstate/snapshots/`. Flaky detection: same gate red→green with zero diff change across attempts → evidence recorded; resolver rule per `spec/RESOLVER.md` W8 row.

**Acceptance:**
- Run: chaos quarantine scenario
- Expected: snapshot bundle exists containing the injected failure string; flaky fixture takes the W8 resolver path

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 9: X7 review diversity

**Wave:** 5 · **Blocks:** Task 10 · **Blocked by:** —

**Files:**
- Modify: `lib/risk-router.sh`, `src/engine/lease.js` (review dispatch selection).

**Contract (handoff plan X7):** Reviewer adapter+model MUST differ from that attempt's implementer (fixer counts as implementer). Single-adapter case → WARN `review-diversity-unavailable`, proceed, journal degradation — NEVER block on diversity.

**Acceptance:**
- Run: dispatch-selection unit test + single-adapter fixture
- Expected: differing model selected when available; degradation WARN journaled otherwise

- [ ] Tests · [ ] Implement · [ ] Acceptance · [ ] Commit

### Task 10: Ungate P2 chaos assertions (full set)

**Wave:** 6 · **Blocks:** — · **Blocked by:** Task 4, Task 6, Task 7, Task 8, Task 9

**Files:**
- Modify: `test/chaos/scenarios/` — flip remaining P2 assertions live per `spec/CHAOS-SUITE.md` phase map (hostile-agent suite, hung-agent, gate-red resolution, review-proof).

**Acceptance:**
- Run: `bash run-tests.sh`
- Expected: full suite + all P0–P2 chaos green, zero unexplained warnings

- [ ] Ungate, run, commit
