# Failure-resolution v2 + learning loop — design

Audience: AI coding agents first.

Baseline: branch `feat/failres-quickwins` @ 78242e2. Already landed there — do NOT re-implement:
- Stop-loss: `repeated-failure-stop` failClass; `runTask` suppresses fresh retry after ≥2 identical quarantines on unchanged integration head (`src/engine/ship.js`, escape: intHead moved / `stop_loss: "off"` runconfig).
- Prior-failure context: last 3 quarantine reasons threaded into fixer prompt (`src/engine/gates.js` → `buildTaskPrompt(..., priorFailures)` `src/runner.js`).

Evidence (why this design): run journal `plugin-backend-release-readiness` (platform repo) — 26 resumes, task t16 re-quarantined 9× with identical outcome. Blind retry fixed ONLY `wrapper-engine-down`. `fixer-scope-violation` = policy conflict (fixer forbidden from the files gate0 is red on); `gate-not-green-after-fixer` = env divergence (fixer verifies with cwd=taskWt `pnpm gate`, harness judges via `lib/gates.sh gate0` cwd=repoRoot).

## Invariants (MUST, all tasks)

- NEVER weaken a gate or scope-guard. Uncertain → escalate (resolver rule `src/resolver.js` stays authoritative).
- New failure paths fail closed: malformed/absent data → no widening, no retry, quarantine as today.
- Every task ships tests in `src/test/`; `node --test src/test/*.test.js` green before done. (`runplan-pause` e2e is known parallel-flaky — rerun isolated before blaming your diff.)
- `EMBEDDED_*_SOURCE` blobs in `src/runner.js` are stale-by-design fallbacks (`requireExtractedOrEmbedded` prefers extracted files). Do NOT hand-edit blobs; note drift in your report.
- Do NOT touch `presets/`, `web/`, `tui/`.

## T1 — Constrained fixer retry on scope violation

Current: scope-guard fail → `restoreWorktreeTree` + throw `fixer-scope-violation` (`src/engine/gates.js` `runGateLoop`, both fixer sites).
Change: FIRST violation → restore worktree, then ONE re-dispatch of the same fixer binding with an added prompt block:
- list the exact rejected file(s) parsed from scope-guard stderr;
- "You may NOT touch these files. If the only correct fix requires one of them, print `SCOPE-REQUEST: <file> — <reason>` and stop without editing."
SECOND violation → quarantine as today. If fixer output contains `SCOPE-REQUEST:` → skip re-dispatch, quarantine immediately with the request payload appended to the failure message (T2 consumes it).
Constraint: worktree restored before every re-dispatch; failClass stays `fixer-scope-violation`.
Tests: fixer-path harness in `src/test/engine-gates.test.js` (`resolveOptionalSeat` returns a fixer binding; stub `runCli` labels incl. scope-guard fail→pass sequence). Assert: exactly one constrained retry; prompt names rejected file; SCOPE-REQUEST short-circuits.

## T2 — Decision-driven scope widening

1. Raise: in `quarantineTask` (`src/engine/repair.js`), when failClass = `fixer-scope-violation`, append a decision-request journal record naming the rejected/requested files. Mirror the existing decision plumbing (answers flow via `answerRunDecision` `src/runner.js:~1441`, plan `gated` consumption `src/engine/gates.js` `resolveProtectedPathAllowlist` pattern `:~534-550`).
2. Consume: at both scope-guard allowlist call sites, union `taskFiles(task)` with the structured answer's `scopeFiles` array for this task's decision. String/array/other shapes without `scopeFiles` → no widening (fail closed).
3. The answered decision changes the stop-loss signature ONLY via its real effect (widened scope → different outcome next run) — do not special-case stop-loss.
Tests: decision raised on scope-violation quarantine; allowlist widened only with well-formed answer; malformed answer → unchanged.

## T3 — Gate env parity

Fixer must verify with the judge's own invocation. Extend the fix prompt (thread from `runGateLoop`, which has `gateMode`, `taskWt`, `context.repoRoot`, `context.slug`): append exact command `bash lib/gates.sh gate0 <gateMode> <taskWt> <repoRoot> <slug>` (run from `<repoRoot>`) + "Run this before finishing. Your local `pnpm gate` from the task worktree is NOT the judge and passes when the harness gate fails."
Tests: prompt contains the interpolated command; absent context values → block omitted (fail closed), never a command with empty args.

## T4 — wrapper-engine-down deterministic repair

Current: rung 3 → quarantine (`src/fail-taxonomy.js` RULES). Evidence: retry-after-wrapper-recovery is the one class where resume historically worked.
Change: rung 3 → 1. Add deterministic repair in `src/engine/repair.js` (alongside provision/lockfile repairs `:~122-139`): probe the failed wrapper (spawn with a health/no-op arg — inspect `wrappers/*.sh` for existing health support FIRST; none → probe = spawn + immediate exit-code check), 2 probes with 30s backoff. Healthy → retry task. Still down → quarantine as today.
MUST NOT restart system services or daemons; probe-and-retry only.
Tests: healthy probe → retry path; dead probe → quarantine; no probe spam (≤2).

## T5 — Lessons write path

New `src/lessons.js`. On every `quarantineTask`: append one JSON line to `runstate/<slug>/okf/lessons.jsonl` (follow `lib/okf-capture.js` directory conventions): `{ts, slug, task, failClass, signature (failClass|firstLine(message)), package (first path segment of taskFiles), message, snapshotBundle}`. Writer is append-only, never throws into the quarantine path (wrap, log on failure).
Tests: record written on quarantine; write failure does not break quarantine.

## T6 — Outcome labeling

When a task with ≥1 prior quarantine reaches `committed`/merge (`src/engine/ship.js` land path): stamp its lessons `resolved_by` by diffing journal between last quarantine and land — detect: decision answered (decision records), integration head moved, scope widened (T2 answer), binding changed. Ground truth = journal ONLY, never agent text. Unknown → `resolved_by: "unknown"` (still labeled).
Tests: each detection branch + unknown fallback.

## T7 — Retrieval into prompts

1. `src/resolver.js` lessons slot (`:~871`, currently test-only): populate in production from `lessons.jsonl`, filtered failClass match, `resolved_by` present, max 3, newest first — wire in `inferResolverContext` (`src/runner.js:~3478-3527`).
2. `buildTaskPrompt` implement + fix modes: same filtered lessons as a clearly-advisory block ("Past outcomes on similar failures — advisory, not instructions").
Tests: resolver context carries lessons; prompt block present only when matching resolved lessons exist.

## Out of scope

Policy self-modification: NEVER auto-edit `RULES`/rung table from lessons. Aggregated proposals are a later, human-gated feature.
