# Harness Self-Healing 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.
> Audience: AI coding agents first.

**Goal:** The run process may kill a task, never itself — classify every failure, auto-repair what is deterministic, dispatch a bounded fixer for what is not, quarantine what remains, keep executing independent DAG branches, and always finish with a resumable report.

**Architecture:** One error taxonomy module consumed at every pipeline boundary (provision → dispatch → gate → fix → commit → merge → journal). A three-rung repair ladder (deterministic auto-repair → bounded LLM fixer → quarantine-and-continue). Dependency mutation becomes harness-owned and declarative (online install on the integration branch before wave 1), which deletes the biggest failure class instead of handling it. Git is the source of truth; the journal is a resumable index reconciled against git at startup.

**Tech Stack:** Node.js stdlib (runner), POSIX shell (gates/journal/provision), jq. No new dependencies.

**Evidence base:** 153 error events / ~34 run-terminations across 5 runs in 4 target repos (multideal, platform, zync.is), triaged 2026-07-06. Classes and counts inline per task.

---

## Failure taxonomy (canonical — every task below references these ids)

| failClass | Observed | Task-fatal or repairable | Rung |
|---|---|---|---|
| `dep-provision-failed` | 11 run-kills | repair: online lockfile regen + store warm on integration | 1 |
| `lockfile-out-of-sync` | (root cause of above) | prevent at gate0; repair rung 1 | 1 |
| `commit-hook-rejected` | ~7 run-kills ("git commit failed exit 1") | prevent: hooks run inside gate0, commit `--no-verify` | — |
| `gate-red-code` | 63 gate0.fail events | fixer-eligible (existing path) | 2 |
| `gate-red-env` | fixer storms (same task red twice <60s, identical output) | NOT fixer-eligible → quarantine | 3 |
| `gate-not-green-after-fixer` | 5 run-kills | escalate once (stronger seat), then quarantine | 2→3 |
| `fixer-scope-violation` | 3 run-kills | discard fix, quarantine task | 3 |
| `merge-conflict` | 3 run-kills | bounded LLM merge resolution, else quarantine | 2→3 |
| `wrapper-engine-down` | 2 run-kills | retry w/ backoff → fallback binding → quarantine bound tasks | 1→3 |
| `wrapper-timeout-repeated` | 5 task-fails | existing fallback; exhausted → quarantine not run-death | 3 |
| `journal-state-unknown` | 2 run-kills | reconcile from git reality, never die | 1 |
| `journal-append-failed` | 1 run-kill | bounded retry w/ backoff under lock | 1 |
| `runconfig-read-failed` | 8 task-fails | prevent: immutable per-run snapshot | — |
| `registry-auth-missing` | anticipated (near-miss 2026-07-06) | pre-flight when deps block present | pre-flight |
| `disk-space-low` | anticipated | pre-flight free-space check | pre-flight |
| `plan-invalid` (dup ids, dangling deps, same-wave file overlap) | anticipated | fail fast at load, before any dispatch | pre-flight |
| `base-moved` (user commits to target main mid-run) | anticipated | pin base SHA at run start; report divergence at land | pre-flight |
| `orphan-worktree` (crash leftovers) | evidenced by manual `rescue/worktree-*` branch | startup sweep | 1 |

**Run-fatal remains ONLY:** journal corrupt beyond reconciliation; target not a git repo; plan-invalid at load. Everything else is task-scoped.

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | T1, T2, T3, T4 | `src/fail-taxonomy.js`(new), `lib/journal.sh`, `lib/gates.sh`, `lib/provision-deps.sh` | ✅ no overlap |
| 2 | T5 | `src/runner.js` | single task |
| 3 | T6 | `src/runner.js` | single task |
| 4 | T7 | `src/runner.js`, `bin/convert-codex-plan-to-harness.js` | single task |
| 5 | T8 | `src/runner.js` | single task |
| 6 | T9 | `src/runner.js` | single task |
| 7 | T10 | `src/runner.js` | single task |
| 8 | T11 | `test/self-healing-integration.sh`(new), `docs/design.txt` | single task |

Waves 2–7 are serialized ONLY because they all modify `src/runner.js` — semantic order also holds (T6..T10 consume T5's quarantine primitive).

---

## Tasks

### Task T1: Error taxonomy module

**Wave:** 1
**Blocks:** T5 | **Blocked by:** —

**Files:**
- Create: `src/fail-taxonomy.js` — single source of truth for failure classification.
- Test: `src/test/fail-taxonomy.test.js`

**Contract:**
- `classifyFailure(error, context) -> { failClass, scope, rung }`
  - `error`: an `Error` whose `.message` matches the runner's existing thrown shapes (e.g. `dep-provision-failed: <task>`, `merge-conflict: <task>`, `wrapper-engine-down: <wrapper>`, `gate-not-green-after-fixer: <task>`, `fixer-scope-violation: ...`, `git commit failed with exit <n>`, `journal append failed with exit <n>`, `unknown journal state: <s>`, `fallback-exhausted: <task>`, `Failed to read runconfig`).
  - `context`: `{ gateHistory?: [{ts, message}] }` — used ONLY to distinguish `gate-red-code` from `gate-red-env` (rule: same task red ≥2 times within 60s with byte-identical first line → `gate-red-env`).
  - `scope`: `"task" | "run"`. `"run"` ONLY for: journal corrupt, not-a-git-repo, plan-invalid. Every failClass in the taxonomy table above is `"task"` except those three.
  - `rung`: `1 | 2 | 3` per taxonomy table; `0` for prevent-only classes (never reaches ladder).
  - Unknown/unmatched error → `{ failClass: "unknown", scope: "task", rung: 3 }` — fail-closed to quarantine, NEVER to run death.
- Module also exports `FAIL_CLASSES` (frozen object of ids) so runner code references constants, not strings.
- Subsume existing `classifyGateFailure` (runner.js:533) semantics: its `discovered-check` / `infra` / `unknown` triage becomes `gate-red-code` / `gate-red-env` / `unknown`.

**Behavior:** pure function, no IO, no throw. Every message shape above maps to its taxonomy row; assert one test per row.

**Acceptance:**
- Run: `node --test src/test/fail-taxonomy.test.js`
- Expected: PASS — one test per failClass id in the table, plus unknown→quarantine default.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add src/fail-taxonomy.js src/test/fail-taxonomy.test.js && git commit -m "feat(taxonomy): failure classification module"`

### Task T2: Journal states `quarantined` + `repaired`, append retry

**Wave:** 1
**Blocks:** T5 | **Blocked by:** —

**Files:**
- Modify: `lib/journal.sh` — `_journal_valid_state()` (line 21) accepts `quarantined` and `repaired`; `append` gains bounded lock retry.
- Test: `lib/test-journal.sh`

**Contract:**
- `_journal_valid_state`: existing states unchanged; add exactly `quarantined`, `repaired`.
- `append`: on lock-acquisition failure retry up to 5 times with 200ms×attempt backoff, THEN exit 3 as today. Success path byte-identical to current behavior.
- `repaired` record carries the standard required fields; runner will put repair detail in a free `detail` field (append must not reject unknown extra fields — verify it doesn't today, keep it that way).

**Behavior:** no other state-machine change; downstream `state`/`resume` readers treat unknown-to-them states as inert history (they already read latest-per-task).

**Acceptance:**
- Run: `bash lib/test-journal.sh`
- Expected: PASS — includes new cases: append `quarantined` ok, append `repaired` ok, append succeeds after 2 simulated lock collisions.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add lib/journal.sh lib/test-journal.sh && git commit -m "feat(journal): quarantined/repaired states + append lock retry"`

### Task T3: gate0 — lockfile-sync check + target-repo commit hooks inside the gate

**Wave:** 1
**Blocks:** T5 | **Blocked by:** —

**Files:**
- Modify: `lib/gates.sh` — two new checks appended to gate0's check sequence.
- Test: `lib/test-gates.sh`

**Contract:**
- **Lockfile-sync check** — runs ONLY when the task diff (vs task base) touches a dependency manifest (`package.json` of any workspace). Offline check per detected manager:
  - pnpm: `pnpm install --lockfile-only --frozen-lockfile` exit 0 (frozen mode errors iff manifest↔lockfile drift; no network).
  - npm: `npm install --package-lock-only --dry-run` then `git diff --quiet -- package-lock.json`.
  - yarn/bun: equivalent lockfile-only sync probe for the detected manager.
  - Drift → gate0 RED with first line `gate0: lockfile-out-of-sync: <manifest path>` (exact literal prefix — taxonomy T1 matches on it).
- **Commit-hook check** — if target repo has `core.hooksPath` or `.git/hooks/pre-commit` executable: run the pre-commit hook against the staged task diff inside gate0 (stage → run hook → unstage, or `git hook run pre-commit` where available). Hook nonzero → gate0 RED with first line `gate0: pre-commit-hook-rejected: exit <n>` + hook stderr appended.
- Both checks are fixer-eligible RED (they are properties of the task's own diff).

**Behavior:** repos with no manifest change / no hooks → both checks no-op, gate0 output unchanged. Never network. Never mutate the worktree (restore staging state on all paths, incl. failure).

**Acceptance:**
- Run: `bash lib/test-gates.sh`
- Expected: PASS — new cases: manifest edit w/o lockfile → RED `lockfile-out-of-sync`; manifest+lockfile consistent → green; failing pre-commit hook → RED `pre-commit-hook-rejected`; hook passes → green; no-hook repo → unchanged.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add lib/gates.sh lib/test-gates.sh && git commit -m "feat(gate0): lockfile-sync + pre-commit-hook checks"`

### Task T4: provision-deps — online repair mode + store warm

**Wave:** 1
**Blocks:** T6 | **Blocked by:** —

**Files:**
- Modify: `lib/provision-deps.sh` — new subcommands beside `detect|provision`.
- Test: `lib/test-provision-deps.sh`

**Contract:**
- `provision-deps.sh repair <workspace>` — ONLINE. Per detected manager, regenerate lockfile from manifest and warm the store: pnpm `pnpm install --lockfile-only && pnpm fetch`; npm `npm install --package-lock-only && npm ci --dry-run`; yarn/bun equivalents. Exit 0 on success; exit 17 (`DEP_PROVISION_FAILED`) with detail on failure. Prints `repaired: lockfile` and/or `repaired: store` lines for the runner to journal.
- `provision-deps.sh warm <workspace>` — ONLINE store-warm only (`pnpm fetch` / manager equivalent), lockfile untouched. Same exit contract.
- Both refuse to run if `RUNPLAN_ALLOW_ONLINE != 1` in env (runner sets it ONLY around rung-1 repair and the T7 deps step) — fail-closed exit 2 otherwise.
- Existing `provision` subcommand behavior byte-identical (still offline, frozen).

**Behavior:** repair/warm run in the given workspace directly (integration worktree), NOT in the staging copy. Serialize under the same global provision lock the runner already holds.

**Acceptance:**
- Run: `bash lib/test-provision-deps.sh`
- Expected: PASS — new cases: `repair` blocked without `RUNPLAN_ALLOW_ONLINE=1` (exit 2); `repair` invokes manager with lockfile-only args (mock manager, as existing tests do); `warm` invokes fetch; `provision` cases unchanged.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add lib/provision-deps.sh lib/test-provision-deps.sh && git commit -m "feat(provision): online repair/warm subcommands, fail-closed env gate"`

### Task T5: Runner — quarantine engine, DAG pruning, end-of-run report

**Wave:** 2
**Blocks:** T6, T7, T8, T9, T10 | **Blocked by:** T1, T2

**Files:**
- Modify: `src/runner.js` — task-failure path in `createRunner`/`runTask` (runner.js:99–436), run completion (runner.js:163).
- Test: `test/runner-integration.sh` (extend existing)

**Contract:**
- New function `quarantineTask(context, task, failClass, detail)`:
  - appends journal state `quarantined` with `detail` field `{failClass, message}`;
  - appends runlog kind `task.quarantined`;
  - marks `task.id` + every transitive dependent (via task `deps` edges AND later-wave same-plan ordering where deps are absent) as `skipped-quarantined` — those get runlog `task.skipped` records, no journal writes;
  - the wave scheduler continues with all unaffected tasks.
- Task-level catch in the scheduler: `classifyFailure(error, ctx)` (T1). `scope === "task"` → quarantine path above. `scope === "run"` → current throw (the only remaining run-death).
- Run completion ALWAYS emits `run.done` with a summary record: `{ landed: [taskIds], quarantined: [{task, failClass, message}], skipped: [taskIds] }`, prints a human table to stdout, and exits 0 if `quarantined.length === 0`, exit 4 (new: partial) otherwise. NEVER `run.error` for task-scoped failures.
- Resume (existing `runplan <slug>` path): latest-state `quarantined` is re-runnable — treated like never-started (fresh lease), its `skipped-quarantined` dependents re-eligible.

**Behavior:** with concurrency>1 an in-flight sibling task is never cancelled by another task's quarantine; pruning applies to not-yet-started tasks only.

**Acceptance:**
- Run: `bash test/runner-integration.sh`
- Expected: PASS — new scenario: 4-task plan (t1→t2, t3→t4), t1 forced to fail provision → t1+t2 quarantined/skipped, t3+t4 land, exit 4, report lists both sets; re-run resumes only t1+t2.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add src/runner.js test/runner-integration.sh && git commit -m "feat(runner): quarantine-and-continue + partial-run report"`

### Task T6: Runner — repair ladder wiring (rung 1 dep repairs, rung 2 escalation, gate triage)

**Wave:** 3
**Blocks:** — | **Blocked by:** T4, T5

**Files:**
- Modify: `src/runner.js` — `provisionDeps` (1232), `runGateLoop` fixer triage (438–514).
- Test: `test/runner-integration.sh`

**Contract:**
- `provisionDeps` failure → rung 1: acquire provision lock, run `lib/provision-deps.sh repair <intWt>` with `RUNPLAN_ALLOW_ONLINE=1` scoped to that subprocess env; on success journal `repaired` (detail `{failClass:"dep-provision-failed", action:"repair"}`), commit any lockfile change on the integration branch as `runner repair: lockfile sync`, re-provision, continue task. On repair failure → quarantine (`dep-provision-failed`).
- Gate triage BEFORE fixer dispatch: `classifyFailure` with gateHistory; `gate-red-env` → NO fixer dispatch, quarantine directly (kills fixer storms: 63 gate0.fail events, zync B4 red twice in 9s).
- Fixer escalation: after `FIXER_MAX_ATTEMPTS` (1) with the task's own seat fails re-gate, ONE extra attempt with the `fixer` seat resolved at tier `high` (existing `resolveSyntheticSeat`), THEN quarantine (`gate-not-green-after-fixer`). No more run-death at runner.js:509/514.
- `fixer-scope-violation`: discard fixer diff (existing snapshot restore), quarantine task — replaces the throw at runner.js:501.

**Behavior:** every rung transition journals + runlogs; a repair that already ran once for the same failClass on the same task does NOT loop — second occurrence goes straight to next rung.

**Acceptance:**
- Run: `bash test/runner-integration.sh`
- Expected: PASS — scenarios: provision fail + successful mock repair → task lands, journal has `repaired`; provision fail + repair fail → quarantined; env-red gate → no fixer dispatch, quarantined; scope-violation → quarantined, siblings land.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add src/runner.js test/runner-integration.sh && git commit -m "feat(runner): repair ladder — dep auto-repair, gate triage, fixer escalation"`

### Task T7: Runner — declarative deps block + pre-flight checks

**Wave:** 4
**Blocks:** — | **Blocked by:** T6

**Files:**
- Modify: `src/runner.js` — plan meta consumption in `loadPlan` (989) + pre-wave-1 step in `main`/`createRunner`.
- Modify: `bin/convert-codex-plan-to-harness.js` — parse plan-doc `deps:` block into plan meta.
- Test: `test/runner-integration.sh`, `src/test/plan-wave.test.js`

**Contract:**
- Plan meta gains optional `deps: [{ workspace: "<rel dir>", add: ["pkg@range", ...] }]`.
- When present, BEFORE wave 1 and after integration worktree creation: runner runs `<manager> add <pkgs>` + install in `<intWt>/<workspace>` with `RUNPLAN_ALLOW_ONLINE=1`, commits `package.json` + lockfile on the integration branch as `runner deps: <pkgs>`, then `provision-deps.sh warm`. Failure → run-level abort BEFORE any task starts (this is the one acceptable early death: nothing to quarantine yet, nothing lost).
- Pre-flight (always, at run start): (a) if `deps` block present or any `.npmrc` in target uses `${NODE_AUTH_TOKEN}`-style env auth → assert the env var is set, else abort with `registry-auth-missing` before any dispatch; (b) free disk space at repo mount ≥ 2 GiB else abort `disk-space-low`; (c) pin `baseSha` at run start in the journal's first record — at land/report time, if target main moved, print divergence warning (no abort).
- Plan-load validation (fail fast, `plan-invalid`): duplicate task ids; `deps` edges referencing unknown ids; two same-wave tasks whose declared Files lists overlap → hard error listing the pairs.

**Behavior:** plans without a `deps` block behave exactly as today. Agents never run installs: dispatch prompt (buildTaskPrompt, runner.js:~1448) gains one line stating the sandbox contract — offline, read-only `node_modules`, dependency changes belong in the plan `deps` block.

**Acceptance:**
- Run: `bash test/runner-integration.sh && node --test src/test/plan-wave.test.js`
- Expected: PASS — scenarios: plan with deps block → integration branch has `runner deps:` commit before t1 lease; duplicate task id plan → load error; same-wave file overlap → load error naming pair; missing NODE_AUTH_TOKEN with env-auth .npmrc → abort pre-dispatch.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add src/runner.js bin/convert-codex-plan-to-harness.js test/runner-integration.sh src/test/plan-wave.test.js && git commit -m "feat(runner): declarative deps block + pre-flight gates + plan validation"`

### Task T8: Runner — mechanical commit + merge-conflict rung

**Wave:** 5
**Blocks:** — | **Blocked by:** T5, T3

**Files:**
- Modify: `src/runner.js` — `commitTask` (749), `mergeTaskIntoIntegration` (768).
- Test: `test/runner-integration.sh`

**Contract:**
- `commitTask`: `git commit --no-verify -m "runner <task.id>"` — hooks already adjudicated inside gate0 (T3). Commit failure now → `classifyFailure` → quarantine, not run death.
- `mergeTaskIntoIntegration` conflict path: instead of immediate `merge-conflict` throw — leave the conflicted merge in `intWt`, dispatch ONE fixer-seat resolution with prompt contract: resolve ONLY conflicted files (list them), no other edits; scope-guard = conflicted-file list; re-verify with `git diff --check` + gate0 on the merged tree; on green `git commit --no-verify` the merge, else `git merge --abort` + quarantine (`merge-conflict`).
- Merge-resolution dispatch is bounded: 1 attempt, task's fixer binding, timeout from binding.

**Behavior:** clean-tree commit path (749–758) unchanged. Merge lock (`withWorktreeLock`) held across the whole resolve-or-abort sequence.

**Acceptance:**
- Run: `bash test/runner-integration.sh`
- Expected: PASS — scenarios: hook-rejecting repo commits fine via runner (hook enforced at gate0 instead); forced conflict + mock fixer resolving it → task lands with merge commit; mock fixer failing → merge aborted, task quarantined, integration branch clean (`git status` empty, no MERGE_HEAD).

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add src/runner.js test/runner-integration.sh && git commit -m "feat(runner): no-verify mechanical commit + bounded merge-conflict resolution"`

### Task T9: Runner — adapter health pre-flight, fallback-exhausted → quarantine, runconfig snapshot

**Wave:** 6
**Blocks:** — | **Blocked by:** T5

**Files:**
- Modify: `src/runner.js` — pre-flight in `main` (19–98), `dispatchWithFallback` (581–629), runconfig handling (46–56).
- Test: `test/runner-integration.sh`

**Contract:**
- Pre-flight: for each DISTINCT wrapper referenced by the resolved preset seats, run `<wrapper> --health` once (contract: exit 0 healthy / exit 3 down, per spec/WRAPPER-CONTRACT.md). Down → retry 3× with 10s/30s/60s backoff. Still down: if EVERY seat binding for it has a fallback chain to a healthy wrapper → proceed (log warning); else abort before any lease with `wrapper-engine-down: <wrapper>` (pre-dispatch abort is cheap; mid-run engine-down is handled next line). Wrappers without `--health` support (nonzero on the flag itself) → skip probe, log `health-unsupported`.
- Mid-run: `fallback-exhausted` / `wrapper-engine-down` / `wrapper-timeout-repeated` from `dispatchWithFallback` → quarantine the task (T5), never run death.
- Runconfig: after synthetic-or-loaded resolution (46–56), copy the file to `runstate/<slug>.runconfig.json` and use THAT stable path for every child dispatch (`--runconfig`), deleting only at `run.done`. Kills `Failed to read runconfig` (8 task-fails: PID-tmpfile raced cleanup).

**Behavior:** health pre-flight adds ≤1 probe per distinct wrapper per run; healthy path adds no dispatch latency.

**Acceptance:**
- Run: `bash test/runner-integration.sh`
- Expected: PASS — scenarios: mock wrapper health exit 3 ×4 with no fallback → abort pre-lease; with healthy fallback → run proceeds, tasks land on fallback; mid-run exhausted fallback → task quarantined, siblings land; runconfig path under `runstate/` present during run, gone after.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add src/runner.js test/runner-integration.sh && git commit -m "feat(runner): adapter health pre-flight + fallback quarantine + stable runconfig snapshot"`

### Task T10: Runner — startup reconciler (journal↔git, orphan sweep)

**Wave:** 7
**Blocks:** — | **Blocked by:** T5

**Files:**
- Modify: `src/runner.js` — resume path in `main` before scheduling (readJournal 1025 consumers).
- Test: `test/runner-integration.sh`

**Contract:**
- On every start with an existing journal, BEFORE scheduling:
  - Per task, cross-check journal latest state against git reality: task branch exists? head commit reachable from integration branch? Rules — journal `committed` but commit absent from integration → demote to re-runnable (journal a corrective record, source `reconcile`); journal mid-flight state (`leased`/`implemented`/`gated`/`reviewed`/`fix`) with no live lease → re-runnable; journal state unknown to this runner version → re-derive from git (branch merged → `committed`; else re-runnable). NEVER abort on unknown state (kills `journal-state-unknown` class).
  - Orphan sweep: `git worktree list --porcelain` entries matching this slug's task-worktree naming (`taskWorktreePath` scheme, runner.js:1273) with no corresponding scheduled/running task → `git worktree remove --force` + delete the task branch IF fully merged into integration; not merged → rename branch to `rescue/<slug>--<task>-<shortsha>` and log it (never delete unmerged work).
- Reconciliation decisions all runlogged (`reconcile.*` kinds) — set-and-forget requires the morning-after log to explain itself.

**Behavior:** clean journal+git → reconciler is a no-op with one `reconcile.clean` log line. Journal file unparseable (invalid JSON mid-file) → this is the one journal run-fatal; print exact line number.

**Acceptance:**
- Run: `bash test/runner-integration.sh`
- Expected: PASS — scenarios: journal says committed but integration lacks the commit → task re-runs; stale worktree + merged branch → swept; stale worktree + unmerged branch → rescued (branch renamed, work preserved); unknown journal state → re-derived, run proceeds.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add src/runner.js test/runner-integration.sh && git commit -m "feat(runner): startup reconciler — journal vs git reality + orphan worktree sweep"`

### Task T11: End-to-end failure-injection suite + design doc update

**Wave:** 8
**Blocks:** — | **Blocked by:** T6, T7, T8, T9, T10

**Files:**
- Create: `test/self-healing-integration.sh` — one scripted scenario per taxonomy failClass (mock wrappers/managers as existing integration tests do).
- Modify: `docs/design.txt` — append a `## Self-healing` section: taxonomy table pointer, ladder rungs, quarantine semantics, the run-fatal short-list.

**Contract:**
- Suite asserts, per failClass: the class is (a) prevented, (b) auto-repaired (journal `repaired`), or (c) quarantined with siblings landing — matching the taxonomy table's rung column. A failClass with no scenario = suite failure (enumerate `FAIL_CLASSES` from T1 and require coverage).
- Runs offline, no real model dispatches (mock wrapper scripts).

**Behavior:** suite wired into `run-tests.sh` so it runs with the repo's standard test entry.

**Acceptance:**
- Run: `bash run-tests.sh`
- Expected: PASS — all existing suites + `self-healing-integration.sh` green; coverage assertion lists every taxonomy id.

- [ ] Write tests covering behavior
- [ ] Implement to contract
- [ ] Run acceptance → expected
- [ ] Commit: `git add test/self-healing-integration.sh run-tests.sh docs/design.txt && git commit -m "test: failure-injection suite covering full taxonomy + design doc"`

---

## Notes

- **Landing:** feature branch `harness-self-healing`, merge to `main` locally + push when all waves green (project convention: no PRs).
- **Out of scope:** Web UI surfacing of quarantine/repair events (belongs to the harness-gated plan's UI track — the runlog kinds added here are its data source); cross-CLI adapter work beyond `--health` consumption.
- **Re-validation targets after landing:** re-run `multideal/instant-load-adoption` (deps block replaces its t1), `zync.is/inventory-management` (merge-conflict + engine-down classes), `platform/plugin-backend-core` (dep-provision + commit-hook classes).
