# Mega Plan Harness — Design-Gap + Autonomy Handoff Plan

Audience: a fresh agent with NO prior context. Source design: `docs/design.txt`. This document is authoritative over design.txt where they conflict. Rev 2 (2026-07-07): autonomy is now the primary goal — the harness must run complex DAG plans unattended, self-heal, and self-orchestrate. Today it needs a human-attended Claude session babysitting it (`babysit-runplan` skill); this plan eliminates that.

## Ground rules (project conventions — binding)

- Fail-closed everywhere. Unexpected state → error with detail, never guess/force.
- All runner errors classify through `src/fail-taxonomy.js` (`FAIL_CLASSES`); never ad-hoc string matching.
- Every change lands with tests (`run-tests.sh`, `lib/test-*.sh`, colocated `*.test.js`). Green = clean, no warnings ignored.
- Landing: branch for isolation, merge to `main` locally, push. NO PRs (private single-dev tool).
- Journal (`runstate/<slug>.jsonl`) is the single source of truth for run state; resumable by design; runner reconciles journal against git reachability on every invocation.
- Web stack: Astro + gateway pattern (`web/src/pages/api/_gateway.js`). Follow it.
- Read referenced files before implementing. Contracts in `spec/` are canonical — never re-derive.

## Current state inventory (verified 2026-07-07)

Built — do NOT rebuild:

| Item | Where |
|---|---|
| Plan executor: waves/tasks, journal, resume, per-slug atomic run lock, git-reachability reconcile | `src/runner.js` (~3k lines), `bin/runplan` |
| Self-healing inside a run: taxonomy, 3-rung repair ladder (deterministic → fixer → quarantine), skip-descendants, exit-4 partial-run semantics | `src/fail-taxonomy.js`, `src/runner.js` |
| Liveness sidecar: per-task state (leased/implemented/gated/reviewed/committed) + pid, locked writes, `reap` verb | `lib/liveness` (bash), `runstate/<slug>.live.json` |
| Supervisor: per-run unix socket, verbs register/pause/resume/kill/steer | `src/supervisor.js` |
| Control API: HTTP over runs, token auth, journal/log watchers | `src/control-api.js`, `bin/ensure-control-api.sh` |
| Adapter contract + wrappers (cursor, codex, north) | `spec/WRAPPER-CONTRACT.md`, `presets/adapters.json`, `wrappers/` |
| OKF: capture/project/scope CLI, format + nav protocol | `bin/okf`, `lib/okf-*.js`, `spec/okf-format.md`, `spec/okf-nav-protocol.md` |
| Backlog: lib + web page + gateway routes | `lib/backlog.sh`, `web/src/pages/backlog.astro` |
| Ship: frozen per-project `ship.sh` wrapper, drift check, `ship-init.sh` | `~/.claude/workflows/lib/ship-init.sh`, run-plan skill |
| Dynamic Workflows compat | `src/dynwf-driver.js` |

## PART I — Why the harness needs babysitting (root causes)

Verified against `src/runner.js`, `src/supervisor.js`, `lib/liveness`, and the `run-plan` / `babysit-runplan` skills. "HALT" is NOT an engine state — it is a skill-level concept: the runner exits non-zero and an attended Claude session interprets its stderr, resolves, and re-invokes `runplan`. That session IS the babysitter. Root causes:

- **R1 — Resolution intelligence lives in prose, not code.** HALT semantics, resume rules, the redo-task escape hatch (delete `plan/<slug>--<id>` branch), verify-then-resume after crash — all in `run-plan`/`babysit-runplan` SKILL.md, executed by an LLM session. Nothing in the harness reacts to a non-zero exit.
- **R2 — No process ownership.** `runplan` runs foreground in a session. Session crash/compaction orphans the run; a human must notice and resume. Nothing restarts or resumes automatically.
- **R3 — Human decisions can occur mid-run.** `requires_decision`/gated tasks, ship-method freeze rung 3 (unstamped `land_mode`), preflight questions — all block on `AskUserQuestion` in a live session. Preflight draining exists only as a manual skill (`plan-preflight`).
- **R4 — No stall enforcement.** Liveness sidecar records states and pids, but nothing enforces per-state timeouts or reaps hung agents during a run. A wedged agent hangs the run silently.
- **R5 — Land failures wait for a human.** `ship.sh land` non-zero → HALT → session must diagnose.
- **R6 — Wave barriers, not true DAG.** Scheduler advances by wave; a slow or quarantined task's *wave* blocks all next-wave tasks even when their actual deps are satisfied. Re-planning around it is a human act.
- **R7 — No escalation channel except in-session Q&A.** When judgment is genuinely needed, the only path is a blocking question to a watching human.

Target: the babysitter's entire job becomes harness code, with a bounded LLM resolver where judgment is required, and asynchronous notification (never a blocking prompt) where a human is genuinely required.

## Canonical implementation specs (one source of truth — implement per spec, plan text below is rationale/overview)

- `spec/IMPLEMENTER-NOTES.md` — READ FIRST: identity model (slug/runId/taskId), runner exit-code contract, cross-spec invariants, resolver-as-library transition seam, LLM-implementer failure-mode self-checks, working protocol.
- `spec/RESOLVER.md` — A2 policy table (every fail-class enumerated), LLM rung prompt+schema, breakers, X5 quota, W8 flaky rule.
- `spec/JOURNAL-V2.md` — I2 record grammar/CRC/recovery, I3 intent log, I4 land exactly-once, I9 migration, V6 single-writer port.
- `spec/DAEMON.md` — A1 state machine + tick loop + systemd + re-attach, A5 decision inbox, W5 status cache, W9 notifications, W10 trust ramp, X1/X4/X6 queues.
- `spec/SCHEDULER.md` — A6 readiness + implicit-dep rule + quarantine propagation, X3 dispatch order, X2 amendment.
- `spec/CHAOS-SUITE.md` — P0 harness: scenarios C1–C15, chaos-wrapper vocabulary, phase gate map.
- `spec/SELF-IMPROVEMENT.md` — PART IX / A7-superseding: learnable registries, lesson lifecycle + efficacy math, postmortem attribution, proposal/ledger/auto-revert, `maint-self-improve` plan.

Where a spec and this plan disagree, the SPEC wins. Wrapper-contract changes (V2 usage event, X5 quota mapping + exit-75 supersession, W7 transcript tee, north dialect gap) are specified in `spec/WRAPPER-CONTRACT-AMENDMENTS.md` — one P1 contract rev.

## PART II — Autonomy architecture (the deep fix)

### A1. Run daemon (`harnessd`) — process ownership kills R2

Long-lived per-machine daemon (systemd user unit + `bin/harnessd`), extending the existing supervisor/control-api rather than a new stack. Owns run lifecycle: `launch → monitor → resolve → resume → escalate → report`. Monitors via: runner exit code, journal tail (fs watchers already exist in `src/control-api.js`), liveness sidecar heartbeats. Crash of the daemon itself is safe: per-slug atomic lock + journal make re-attach/resume idempotent (already true of the runner); systemd `Restart=on-failure` covers the daemon. `runplan <slug> --unattended` hands the run to the daemon; foreground mode stays for interactive use.

### A2. Resolution policy engine — codifies the babysitter, kills R1/R5

New module `src/resolver.js`: table-driven `(failClass, attemptCount, context) → action`. Actions: the closed set is canonical in `spec/RESOLVER.md` (resume, redo-task, env-repair:<registry-id>, reroute, park-retry, accept-quarantine-continue, escalate). Encodes the prose rules verbatim as the deterministic tier:

- Non-zero runner exit → classify per `spec/RESOLVER.md` § Failure input contract (structured stderr JSON once the P2 runner change lands; `classifyFailure` on raw stderr as fallback), act.
- Crash/dead pid → verify-then-resume: reconcile journal vs git (engine already does this on invoke) → `resume`.
- Task branch ahead of integration → resume it, never re-implement (engine already does this — resolver just re-invokes).
- Run-fatal shortlist (`journal-corrupt`, `not-a-git-repo`, `plan-invalid`) → `escalate` immediately, no retry.
- NEVER weaken a gate to pass (`.warnignore` rubber-stamp, review skip) — not in the action set at all, by construction.

Middle rung — **LLM resolver** (the meta-orchestrator's first real decision point): when the deterministic table has no rule or a rule already failed once, invoke a single schema-constrained LLM call (adapter-dispatched, cheap model) with stderr JSON + journal tail + task log excerpt; answer must be one of the closed action set + one-line rationale, journaled as `resolve.decision`. LLM unavailable/invalid answer → `escalate` (fail-closed). Budgets: per-task circuit breaker (max N resolution attempts, default 3), exponential backoff, per-run global attempt budget in runconfig. Breaker trips → quarantine (task-scoped) or escalate (run-scoped).

### A3. Preflight gate in the engine — kills R3

`runplan preflight <slug>` (and implied by `--unattended`): refuses launch unless ALL are clean — no OPEN `gated` records, every `requires_decision` bound to a resolved decision, `meta.land_mode` stamped + ship wrapper exists + `ship.sh drift` exits 0, adapter wrappers for the preset respond to a health ping, disk/auth/base-freshness prevent-checks pass (these exist pre-dispatch — surface them at preflight instead). Output: machine-readable list of open items so a planning session can drain them (the existing `plan-preflight` skill becomes a thin UI over this). Guarantee by construction: an unattended run can never need a mid-run human answer.

### A4. Watchdog — kills R4

Per-state max-age budgets in runconfig (e.g. `leased: 10m`, `implemented: 45m`, `gated: 20m`, per-preset overridable) + agent-output idle timeout. Daemon tick reads liveness sidecar; breach → SIGTERM→grace→SIGKILL the recorded pid, classify as new `FAIL_CLASS` `task-stalled`, feed the NORMAL repair ladder (rung 1 retry once → fixer → quarantine). Wire `lib/liveness reap` into the same tick for orphan cleanup. New taxonomy entries: `task-stalled`, `agent-idle`, `land-failed` (so R5 routes through the resolver like everything else).

### A5. Async decision inbox + notifications — kills R7

Escalation is a journal record (`decision.requested {id, task|run, reason, options, default?}`) + push notification (ntfy/webhook — configurable in runconfig), NEVER a blocking prompt. The run continues every task not dependent on the answer; dependents wait in a `blocked-on-decision` state. Answers arrive via control-api endpoint (`POST /runs/:id/decisions/:did`) or CLI (`runplan answer <slug> <did> <choice>`); daemon unblocks and proceeds. Optional per-decision `default` + timeout for decisions the plan author pre-authorizes to auto-resolve. This also becomes the Web UI's decision surface later.

### A6. True DAG scheduling — kills R6

Replace wave-barrier advancement with dependency-edge readiness: a task is dispatchable when all its declared deps are `committed` (waves remain as display/grouping metadata and as implicit deps where a plan declares none). Effects: quarantine skips only true descendants (closer to design intent than today's wave-scoped skip), slow tasks stop blocking unrelated siblings, concurrency cap does the throttling. This is the largest runner change — gate it behind a runconfig flag (`scheduler: "dag" | "wave"`, default `wave` until chaos tests pass), keep journal event shapes identical so resume/UI are unaffected.

### A7. Self-improvement loop — makes healing get better over time

On every quarantine and every successful fixer/resolver repair, write an OKF lesson (job scope, `type: lesson`, tagged by fail-class); `okf promote` to project scope when the same fail-class repeats ≥2×. Resolver and fixer prompts inject matching lessons. Record every resolution outcome (`resolve.outcome {refDecisionSeq, action, success}` — payload canonical in `spec/RESOLVER.md`) so `run.done` reports repair success rate per fail-class — measurable self-healing, not vibes.

### A8. Skills shrink to shims

After A1–A5: `babysit-runplan` reduces to "query daemon status, relay escalations" — the loop logic is deleted from prose. `run-plan`'s launch ladder collapses into `runplan preflight` + `runplan <slug> --unattended`. Prose that stays: user-interaction phrasing only. This is the acceptance signal that autonomy is real: the skill files get *shorter*.

### Autonomy acceptance (chaos test — the epic's definition of done)

Seeded test plan + fault injection harness (`test/chaos/`): one gate-red task, one agent that hangs (sleep forever), one injected runner crash (SIGKILL mid-wave), one deliberately unresolvable task, one `decision.requested` with a pre-authorized default. Run `runplan <slug> --unattended`. PASS = run reaches terminal state with zero interactive prompts; hung task killed + retried; crash auto-resumed without redoing committed work; unresolvable task quarantined with descendants skipped and siblings landed; exit summary + notifications accurate. This test is CI for every autonomy phase.

## PART III — Design corrections (gaps in design.txt — adopt these)

- **D1. "Shared memory" = OKF. Do not build a second system.** Agents read job-scope OKF at spawn (nav-protocol snippet injected), write learnings back via `okf new`/`okf promote`. Last-write-wins per file + mandatory index update; `okf doctor` in gate.
- **D2. Agent communication = journal-backed message bus.** Append-only `messages.jsonl` per run; schema `{id, ts, from, to|broadcast, kind, body, readBy[]}` in `spec/messages.schema.json`; delivery via supervisor relay (the `steer` verb is the seed). Replayable on resume; fail-closed on malformed. NO live socket mesh.
- **D3. Meta-orchestrator = bounded LLM decision points over a deterministic core.** Superseded-and-absorbed by A2 (resolution) and A5 (decisions). A third decision point — plan mutation proposals (split/add task after repeated failure) — journals as `decision.requested`, never auto-applies.
- **D4. Self-improvement** — absorbed by A7.
- **D5. Web UI realtime = SSE tailing journal/log via control-api** (`/runs/:id/events`, agent stream). Prompt-send goes through supervisor `steer` — one write path. No WebSockets.
- **D6. Security.** Token auth forwarded by gateway; localhost bind default. Agent chat input is a prompt-injection surface → gate behind per-run `interactive: true` runconfig flag; journal every injected prompt. Kill = SIGTERM→grace→SIGKILL→`interrupted` fail-class, worktree preserved.
- **D7. Adapter registry stays in `presets/adapters.json`.** Extend schema: `enabled`, `allowedModels` (subset), `healthCheck {cmd, intervalSec}`. Health ping = wrapper no-op per `WRAPPER-CONTRACT.md`; preflight (A3) consumes it.
- **D8. DAG view derives from plan deps + journal status** — one gateway endpoint `/runs/:id/graph`; no persisted graph. A6 makes the engine's true topology match the display.
- **D9. `@platform-modules/mod`:** first UI task = inventory it, report coverage gaps to user before writing custom components (design.txt requires this).
- **D10. OKF cross-CLI sync:** capture adapters only for installed CLIs (codex, cursor); CLIs without a memory equivalent get an explicit no-op module with reason.

## PART IV — Execution plan (phases; each = branch → tests green → chaos test green → merge to main)

Autonomy first — it is the goal; UI observes it; adapters extend it.

- **P0 — Read-in + chaos scaffold.** Read `src/runner.js` (structure), `src/supervisor.js`, `src/control-api.js`, `lib/liveness`, `spec/*`. Build `test/chaos/` fault-injection harness + seeded plan (acceptance above). Nothing merges after this without chaos green for the features it claims.
- **P1 — Preflight gate (A3) + taxonomy additions** (`task-stalled`, `agent-idle`, `land-failed`, `land-blocked-unreviewed`, `protected-path-touched`, `provider-quota`, `budget-exceeded`, `artifacts-missing`, `adapter-contract-violation` — ALL new classes land in one taxonomy rev) + new journal kind `review.passed {task, stage}` written by the runner's review path (v2 slug-journal addition; today review success is only a `state: reviewed` record — the land gate needs the per-stage proof) + wrapper contract rev per `spec/WRAPPER-CONTRACT-AMENDMENTS.md`. Acceptance: plan with one OPEN gated record → `preflight` exits non-zero listing it; clean plan → exit 0; `--unattended` refuses on dirty preflight.
- **P2 — Resolver (A2) + watchdog (A4).** Deterministic table first, then LLM rung with fail-closed fallback. Acceptance: chaos hung-agent and gate-red scenarios pass; with LLM disabled, behavior is deterministic and regression-tested byte-identical for the table-covered classes.
- **P3 — Daemon (A1) + decision inbox (A5).** systemd unit, `--unattended`, notifications, `runplan answer`. Acceptance: full chaos test passes end-to-end unattended, including crash-resume and pre-authorized default decision.
- **P4 — DAG scheduler (A6)** behind `scheduler: "dag"` flag. Acceptance: chaos + a diamond-dependency plan where a quarantine skips only true descendants; wave mode regression-identical.
- **P5 — Lesson loop (A7) + skill shims (A8).** Acceptance: forced quarantine → lesson file → next run's fixer prompt contains it; babysit/run-plan skills rewritten thin.
- **P6 — Message bus (D2).** Acceptance: two stub agents exchange request/response via `messages.jsonl`; malformed message → classified failure.
- **P7 — Control-api read surface (D5, D8) + Web UI screens** (Overview → Job/DAG → Agent → Adapter setup → OKF), platform-modules inventory (D9) first. Decision inbox UI surfaces A5 escalations. Acceptance per screen: Playwright against a seeded run.
- **P8 — Adapters (D7, D10):** schema extension + validation, claude-code + opencode wrappers, health verb; OKF capture for codex/cursor. Acceptance: `presets/_validate.mjs` green; preflight consumes health.

## PART V — Robustness engineering spec

Autonomy without robustness is a fast way to corrupt state unattended. These are the invariants and mechanisms that make an unattended harness trustworthy. Each invariant names its enforcement point; violations are bugs, and where marked, chaos/property tests must prove them.

### Invariants (explicit, enforced, tested)

- **I1 — Single writer per run.** At most one runner process mutates a slug's journal/worktrees. Enforced: existing hardlink lock + stale takeover (`src/runner.js`). Extend: lock records pid + process start-time (from `/proc/<pid>/stat`) so pid reuse can never pass a staleness check.
- **I2 — Journal is append-only, self-verifying, and recoverable.** Each line gains `seq` (monotonic counter, not wall clock — journals must order correctly across clock skew) and a per-line CRC. `fsync` after each append (already retried per taxonomy). Torn final line (crash mid-write) → recover to the last valid prefix and journal a `journal.recovered` record — this demotes `journal-corrupt` from run-fatal to recoverable EXCEPT when corruption is interior (checksum fail on a non-final line), which stays run-fatal + escalate. Property test: truncate/garble the journal at every byte offset of the last record → runner either recovers or fail-closes; never mis-parses.
- **I3 — Every side effect is idempotent or intent-logged.** Git effects are already idempotent by construction (deterministic branch/worktree names, reachability reconcile). Non-git effects (dep provisioning, file moves, lock creation, notifications) get write-ahead intent records: `intent {op, key}` before, `done {key}` after; startup sweep re-drives or rolls back orphaned intents. Kill-matrix test: SIGKILL the runner at every journal event boundary in the chaos plan → resume must converge to the same terminal state with no duplicate side effects (this is the single highest-value robustness test; automate it, don't sample it).
- **I4 — Landed work is exactly-once.** `ship.sh land` runs only after a journaled `land.intent`; re-invocation checks git remote reachability of the integration head before acting (no double-push, no double-PR). Land failure → `land-failed` fail-class → resolver, never a retry loop around a partially-landed state.
- **I5 — Child processes never outlive their task.** Agents spawn via `setsid` into their own process group; kill = signal the group, not the pid (a wrapper that forks children today leaks them past SIGTERM). Liveness sidecar records pgid + start-time. Watchdog reap verifies identity (pid + start-time) before signaling — never kill a recycled pid.
- **I6 — Resource ceilings on everything unattended.** Per-agent: wall-clock timeout (A4), memory/CPU via systemd-run scoped units or ulimit (pick systemd-run — the daemon already assumes systemd), output-size cap on captured logs (truncate + journal, don't OOM on a runaway agent). Per-run: max duration, max total resolution attempts, disk floor (prevent-check exists — enforce continuously in daemon tick, not just at dispatch), LLM token/cost budget. Every ceiling breach routes through the taxonomy, never a raw crash.
- **I7 — External calls are contract-checked and bounded.** Every wrapper invocation: timeout + bounded retry with jittered backoff for transport-class failures ONLY (never retry on gate-red or non-transient classes — retry classification comes from the taxonomy, transport vs semantic). Wrapper stdout must validate against the events schema (`spec/events.schema.json`); malformed output = `adapter-contract-violation` fail-class attributed to the ADAPTER (health-degrade it, A3/D7), not the task.
- **I8 — The daemon is crash-safe and stateless.** All daemon state re-derivable from journals + liveness sidecars + lock files on restart; systemd `Restart=on-failure`; a daemon crash mid-resolution re-enters via the same resolver table (safe because I3). Chaos test includes killing the daemon itself.
- **I9 — Version-skew fail-closed.** Journal and runconfig carry a `formatVersion`. Engine refuses to touch a journal written by a NEWER version (escalate); older versions go through an explicit migration function (tested), never silent best-effort parsing.
- **I10 — Secrets never enter journals, logs, lessons, or notifications.** Redaction pass at the append boundary (env-derived denylist + common token patterns); test with planted fake secrets in agent output.
- **I11 — Notifications degrade, never block.** ntfy/webhook failure → journal `notify.failed` + continue; escalations remain in the journal-backed inbox regardless, so a dead notification channel loses latency, not decisions.

### Robustness test strategy (extends the chaos harness, P0)

1. **Kill matrix** (I3) — SIGKILL at every event boundary, assert convergence. Runs in CI on every autonomy-touching change.
2. **Journal fuzz** (I2) — corruption at last-record byte offsets + interior corruption; assert recover-or-fail-closed.
3. **Hostile agent suite** (I5/I6/I7) — wrapper stub that: forks orphans, emits 1GB of output, emits malformed JSON, sleeps forever, exits 0 with no commit. Each must classify correctly, never wedge the run.
4. **Concurrency suite** (I1) — two runners same slug (second must refuse), stale-lock takeover under pid reuse simulation, two DIFFERENT slugs sharing the machine (must not interfere; `--isolate` path covered).
5. **Soak** — chaos plan on a loop overnight via the daemon; assert zero orphaned worktrees/branches/processes/locks after N cycles (`git worktree list`, `pgrep`, lock dir empty).

### Phase mapping

I1/I2/I3 land in **P2** (they gate the resolver — resuming on a lying journal is worse than halting); I5/I6 in **P2** with the watchdog; I4 in **P2** (`land-failed` path); I7 in **P8** but the schema-validation half earlier wherever wrapper output is first consumed; I8/I11 in **P3**; I9/I10 in **P1** (cheap, foundational). Test strategy items 1–4 are part of P0's chaos scaffold; soak joins P3 acceptance.

## PART VI — Second-order improvements (blind-spot audit, 2026-07-07)

Gaps outside the literal autonomy goal that become acute the moment autonomy works. Each item: what, why, how, acceptance. Priorities: **S** = in-scope for autonomy phases (P1–P3), **M** = own phase, **L** = later.

### V1 (S) — Guardrails replacing the human review layer

The babysitting session is today's implicit human glance over everything a run lands; `--unattended` + merge-to-main removes it. Replace with mechanical guardrails:

- **Protected paths.** New runconfig field `protectedPaths: []` with hard defaults compiled into the engine (non-removable, only extendable): `.claude/`, `hooks/`, `.github/`, `**/ship.sh`, `spec/`, the engine's own `src/` when the target repo IS this harness, `.warnignore`. Enforcement point: post-implement gate in `src/runner.js` — `git diff --name-only <integration>..<task-branch>` intersected with the protected set; any hit → fail-class `protected-path-touched` (new `FAIL_CLASSES` entry, prevent-band, NOT fixer-repairable — a fixer "repairing" a gate it failed is the exact attack this blocks) → quarantine + escalate. A plan may whitelist a specific protected path for a specific task via an explicit `gated` decision resolved by the user at preflight — never at run time.
- **Review-passed proof before land.** `ship.sh land` is invoked by the engine only after the journal shows `review.passed` for every landed task (both review stages). Implementation: land step reads the journal, not in-memory state, so a resumed run cannot skip it. Acceptance: chaos scenario — task branch manually committed without review events → land refuses with `land-blocked-unreviewed`.
- **Morning digest.** Daemon emits per-run digest on terminal state AND a daily rollup (systemd timer): commits + diffstat per task, quarantines with fail-class, `.warnignore` lines added this run (already surfaced by gate0 — aggregate them), decisions auto-resolved by default, cost (V2). Delivery via the A5 notification channel; store as `runstate/digests/<date>.md`. Acceptance: chaos run produces a digest listing the injected quarantine and the pre-authorized decision.
- **Egress note.** Full network sandboxing of agent processes is out of scope (agents legitimately need registries/APIs). The compensating control is protected paths + review-proof + digest. Record this tradeoff in the digest header the first time a run executes with network-active agents.

### V2 (S) — Cost metering + budget + cost-aware routing

The operator's realized pain: unattended runs burn tokens blind.

- **Metering.** Extend `spec/WRAPPER-CONTRACT.md` + `spec/events.schema.json`: wrappers MUST emit a final `usage` event `{inputTokens, outputTokens, cachedTokens?, costUsd?, model}` — every supported CLI exposes usage (codex/claude JSON output; cursor session stats); a wrapper that cannot → emits `usage.unknown` and the adapter is marked `metered: false` in `presets/adapters.json` (preflight WARNS when an unattended run uses unmetered adapters). Runner journals `task.usage` per attempt (implement, fix, review are separate records — repair cost must be visible).
- **Budget.** Runconfig `budget: {maxUsdPerRun?, maxTokensPerRun?, maxUsdPerTask?}`. Enforcement: daemon tick sums `task.usage`; task ceiling breach → fail-class `budget-exceeded` (task-scoped, quarantine); run ceiling breach → pause run + `decision.requested` (continue with raised budget / abort) — pause not kill, so committed work stays resumable.
- **Routing.** `lib/risk-router.sh` currently routes by risk tier; add a cost dimension: per-adapter-model `costTier` in `presets/adapters.json` and route to the cheapest model whose historical gate-pass rate for that task tier ≥ threshold (data from V4 stats; until stats exist, static tier table). Keep the router a pure function: `(taskTier, statsSnapshot) → adapterModel`, unit-tested with fixture snapshots.
- Acceptance: chaos run's `run.done` and digest show per-task and total cost; a scenario with `maxUsdPerTask: 0.01` quarantines the expensive task.

### V3 (M) — Plan quality gate (`runplan lint`) + authoring feedback loop

Unattended-run quality is bounded by plan quality; a bad plan becomes quarantines no resolver fixes.

- **`runplan lint <slug>`** (also auto-run inside `preflight`): dep-graph checks (cycles, unknown ids, unreachable tasks, single mega-task waves), task-shape heuristics (description length floor, verifiable acceptance line present, files-touched hint present), decision hygiene (verbs like "choose/decide/pick/ask" in a task description without a bound `gated` record → ERROR — this closes the run-plan skill's documented "no LLM scan backstop" gap deterministically first), meta hygiene (`land_mode` stamped, preset exists). Output: machine-readable findings `{severity, taskId, rule, message}`; ERRORs block `--unattended`.
- **Optional LLM lint rung** (same pattern as A2): one schema-constrained call per plan flagging likely-underspecified tasks; WARN only, never blocks, fail-open to skip when LLM unavailable.
- **Feedback loop:** V4's stats identify which plans/task shapes quarantine; `runplan lint --learn` emits an OKF lesson (project scope, `type: lesson`, tag `plan-authoring`) when a lint-clean plan still quarantined — the rule gap itself is the lesson. Plan-authoring skills (brainstorm/plan) read these lessons.
- Acceptance: fixture plans (cyclic deps, undeclared decision, mega-task) each produce the expected finding; the chaos plan lints clean.

### V4 (M) — Cross-run analytics (`runplan stats`)

Self-improvement claims (A7) are unmeasurable without baselines; routing (V2) and lesson promotion need data.

- **Implementation:** `src/stats.js` + `runplan stats [--since DATE] [--json]` — a pure reducer over `runstate/*.jsonl` (append-only journals ARE the database; no new store). Metrics: quarantine rate by fail-class over time, repair success per ladder rung, task duration percentiles by tier, adapter/model gate-pass rate, cost per landed task (V2 data), decision latency (requested→answered). Snapshot export `runstate/stats-cache.json` (regenerated, gitignored) so the router (V2) and lesson promotion (A7) read a file, not recompute.
- Journals must therefore carry adapter/model per attempt — add to `task.usage`/state records if absent.
- Acceptance: reducer unit-tested against fixture journals with known aggregates; `stats --json` output schema-validated.

### V5 (M) — Portability: vendor the machine-local brain

The engine is portable; its dependencies (`~/.claude/workflows/lib/*.sh` — ship-init, rp-isolate; PATH-installed `runplan`; frozen per-repo `ship.sh`) are one-machine prose. Machine dies → harness unrecoverable from the repo.

- Vendor load-bearing scripts into `vendor/claude-workflows/` (copied, versioned, with an upstream-sync check script that diffs against `~/.claude` and reports drift — single source of truth becomes the REPO, `~/.claude` becomes the deploy target); `bin/harness-init.sh` grows to a full fresh-machine bootstrap: install `runplan` to PATH, deploy vendored scripts, verify with a self-test (`harness-init.sh --verify` runs the chaos plan's smallest scenario).
- Acceptance: CI job (or manual gate) that runs `harness-init.sh --verify` in a clean container with only git+node+jq present.

### V6 (S) — Single-writer state: retire dual bash/node implementations

`lib/journal.sh` and `src/runner.js` both write the journal; `lib/liveness` (bash) writes JSON that the Node daemon reads. Dual-language mutation WILL drift (locking semantics, JSON edge cases, fsync behavior).

- Rule: **Node is the only writer** of `runstate/*` files. Port `lib/liveness` and `lib/journal.sh` write-paths to `src/state/` modules (this is also where I2's seq+CRC lands — do them together, one migration); the bash files become ≤10-line shims exec-ing `node src/state/cli.js <verb> ...` so every existing caller keeps working. Read-paths in bash may stay.
- Sequencing: MUST land at the start of P2, before resolver/watchdog build on liveness data.
- Acceptance: existing `lib/test-journal.sh` and liveness tests pass unmodified against the shims; concurrent-writer suite (Part V test 4) passes.

### V7 (S) — Modularize `runner.js` before it grows

3k lines, single file, and P2/P4/P6 all modify it. Split is mechanical now, painful after.

- Target layout: `src/engine/{scheduler,lease,gates,repair,ship}.js` + `src/state/{journal,liveness,lock}.js` (V6) + existing `fail-taxonomy.js`; `runner.js` shrinks to wiring + CLI. Pure refactor: NO behavior change, moves only, public exports of `runner.js` preserved (control-api imports them — keep re-exports). One module per commit so review stays tractable.
- Sequencing: first task of P2, together with V6.
- Acceptance: full existing test suite + chaos scaffold green, `git diff` shows moves not edits (verify with `git log --follow` sanity + no test changes needed).

### V8 (L) — Lifecycle GC (`runplan gc`)

`runstate/` grows forever; task branches/worktrees accumulate.

- `runplan gc [--dry-run] [--retention-days N=14]`: for runs whose journal shows terminal state AND integration branch landed (reachable from origin/main or PR merged): delete task branches `plan/<slug>--*`, remove worktrees, archive journal to `runstate/archive/` (never delete journals — they are V4's database), clear liveness sidecars/locks. Fail-closed: any ambiguity (branch ahead of everything reachable, journal non-terminal) → skip with reason, never force-delete. Dry-run is the default output of the daily digest.
- Acceptance: fixture with one landed, one quarantined, one in-flight run — gc touches only the first; `--dry-run` mutates nothing.

### V9 (L) — Adapter version pinning + semantic smoke

Health pings catch dead wrappers, not upstream CLI flag/output drift (codex/cursor CLIs change frequently).

- `presets/adapters.json` gains `pinnedVersion` + `versionCmd`; preflight compares and WARNs on drift (blocks only in `--unattended` when `strictVersions: true`). Nightly soak (Part V test 5) adds a per-adapter contract smoke: trivial one-task plan through each enabled adapter, asserting schema-valid events + usage record. Drift found → notification, adapter auto-degraded to `enabled: false` only on hard contract violation, WARN otherwise.
- Acceptance: simulated wrapper emitting a renamed field → smoke flags it and degrades the adapter.

### V10 (L) — UI value ordering

Within P7, build in this order: decision inbox (answers A5 escalations — the only UI that unblocks runs) → digest/stats views (V1/V4 render) → Overview tiles → Agent chat → DAG visualization last (garnish, highest effort). Single-user tool: glanceability + unblocking beat eye-candy.

### Phase mapping (delta to PART IV)

- **P1** += V2 metering/budget (schema + journal side), V1 protected-paths fail-class.
- **P2** starts with V6+V7 (state single-writer + runner split — everything later builds on these), then resolver/watchdog; V1 review-proof lands with the `land-failed` path.
- **P3** += V1 digest, V2 budget enforcement in daemon tick.
- **New P4.5 — Plan quality + analytics:** V3 lint (preflight integration back-ports into P1's gate), V4 stats, V2 cost-aware routing (needs V4 snapshot).
- **New P5.5 — Portability:** V5 vendoring + bootstrap verify.
- **P7** ordered per V10. **P8** += V9. V8 anytime after P3 (daemon digest hosts its dry-run report).

## PART VII — UX + observability features (accepted 2026-07-07; `runplan simulate` explicitly REJECTED — do not build)

### W1 — `runplan watch <slug>` (live TUI)

Terminal dashboard tailing the journal + liveness sidecar: wave/task tree with live states, per-agent last journal line, running cost ticker (V2 `task.usage` sum), watchdog time-remaining per active task, pending decisions banner. Implementation: `src/tui/watch.js`, plain ANSI redraw (no curses dep; test with `--once` snapshot mode that renders one frame to stdout for golden-file tests). Data source: same fs-watch machinery as `src/control-api.js` — extract that watcher into `src/state/watch.js` and share; do NOT duplicate tailing logic. Acceptance: golden-frame test against a fixture journal; `--once` on the chaos run mid-flight shows the hung task's countdown.

### W2 — `runplan why <slug> <taskId>` (causal explainer)

Reconstructs a task's full causal chain from the journal as plain text: leased → each attempt (adapter/model, duration, cost) → fail-classes → repair rungs tried → resolver decisions WITH their journaled rationales (A2 `resolve.decision`) → terminal state, plus pointers to the snapshot bundle (W6) and transcript (W7). Implementation: pure function over journal records in `src/explain.js`, unit-tested on fixture journals (one per fail-class path). Output modes: text (default), `--json`. Acceptance: chaos run's quarantined task explains end-to-end without reading raw JSONL.

### W3 — `runplan timeline <slug>` (post-run concurrency chart)

Wall-clock chart per task split into queue-wait / implement / gate / review / repair segments, computed from journal timestamps + seq. Output: ASCII Gantt (default) and `--html` single self-contained file. Implementation: reducer in `src/stats.js` (shares V4's parsing — one journal reader). Purpose: makes scheduler waste visible and P4's DAG-scheduler payoff measurable — run it on the same plan before/after `scheduler: "dag"`. Acceptance: fixture journal with known overlap renders expected segment table (`--json` mode for the test).

### W4 — `runplan doctor` (environment health)

One command: daemon up + socket responsive, per-slug locks sane (pid+start-time alive), orphan worktrees/`plan/*--*` branches/processes/liveness sidecars, adapter health + version drift (V9 data), disk floor, journal `formatVersion` skew, PATH install intact. Output: check-list with PASS/WARN/FAIL + fix hint per line, exit non-zero on FAIL. Implementation: `src/doctor.js`; each check is a named pure-ish function returning `{id, status, detail, hint}` so V5's `harness-init.sh --verify` and the soak can reuse individual checks. Acceptance: seeded orphan worktree + stale lock fixture → exactly those two FAILs.

### W5 — Statusline segment

`runplan status --statusline` prints one compact line: `⏵<active> ✋<pending-decisions> ⚠<quarantines-today>` (empty output when all zero — statuslines omit empty segments). Reads `~/.harness/runs/` pointers + journals; MUST complete <50ms (read pointer files only, never full journals — daemon maintains a `~/.harness/status-cache.json` it rewrites on every event; the command just cats it). Wire into Claude Code statusline / starship as an exec segment. Acceptance: timing test + cache-staleness test (daemon down → segment shows `?` not lies).

### W6 — Failure snapshot bundles

On every quarantine: tar worktree diff vs integration (`git diff` output, not the tree), agent transcript (W7 path), gate/review outputs, journal slice for the task → `runstate/snapshots/<slug>--<taskId>-<attempt>.tar.gz`. Size-capped (I6 output caps apply; truncate with marker). Journaled as `snapshot.written {path}` so W2 can link it. GC (V8) keeps snapshots per retention policy but ALWAYS through archive, never silent delete. Acceptance: chaos quarantine produces a bundle whose contents match a manifest test.

### W7 — Agent transcript archive, indexed

Every attempt's full transcript persists to `runstate/transcripts/<slug>/<taskId>-<attempt>.jsonl` (wrapper contract: wrappers already stream events; tee them). Post-run, daemon indexes transcripts into the OKF/ctx-search store (job scope) so "what did the agent actually do" is searchable. Redaction (I10) applies at write. Acceptance: transcript exists per chaos attempt; planted marker string is findable via the index; planted fake secret is NOT present.

### W8 — Flaky-gate detection

A gate that fails then passes with an IDENTICAL tree (same task-branch commit, no fixer diff between attempts) → journal `gate.flaky {gate, evidence}`; stats (V4) track flake rate per gate/repo. Resolver rule: first flaky-classified failure retries once WITHOUT consuming a repair-ladder rung; repeated flakes → `decision.requested` (fix the test) rather than silent retry-forever. Acceptance: chaos gains a coin-flip gate fixture; classified flaky, run still lands, stats show it.

### W9 — Actionable notifications (+ Tailscale exposure)

A5 escalation notifications carry the decision's options as ntfy action buttons; each button is an HTTP action POSTing `{decisionId, choice}` to the control-api answer endpoint with the auth token. Requirements: control-api reachable from the phone — bind to the Tailscale interface ONLY (tailnet IP or `tailscale serve`), NEVER 0.0.0.0/public; token still required (defense in depth); answers are idempotent (second tap → 409 with the recorded answer, not a re-resolve). Acceptance: integration test with a stub ntfy server asserting button payloads; double-answer test returns 409.

### W10 — Graduated autonomy (trust ramp)

Per-repo `autonomyLevel` in config (V-layered): **0** attended (today's foreground), **1** step-mode — run pauses at each wave boundary emitting `decision.requested {kind: "wave-gate"}` answered via inbox/notification, **2** unattended with tight budgets (V2 ceilings required non-null), **3** unattended full. Enforcement in the daemon launch path: requested mode > repo's level → refuse with the stats evidence needed to promote (from V4: e.g. "quarantine rate 12% over last 10 runs; level 3 wants <5%"). Promotion is a human config edit — the harness recommends, never self-promotes. Step-mode is the P2–P3 trust bridge. Acceptance: chaos at level 1 pauses at each wave; level mismatch refusal message includes live stats.

### W11 — OpenTelemetry export

`src/otel-export.js`: converts a journal into OTLP spans (run = trace; task = span; attempts/gates/reviews = child spans; usage + fail-class as attributes) and POSTs to a configured OTLP/HTTP endpoint. Batch-on-terminal by default; `--follow` streaming mode optional later. NO instrumentation inside the engine — the journal is already the event source; this is a pure exporter (keeps the engine dependency-free). Acceptance: fixture journal → span tree asserted via a stub OTLP collector; runs against local Grafana/Jaeger docker for manual verify.

### W12 — Runstate backup

Daily systemd timer (daemon-owned): `runstate/` archives + journals + snapshots → restic repo or private git remote (operator choice in config; implement restic first — content-addressed, encrypted). Journals are the analytics database and audit log (V4) — they must survive disk loss. Backup failure → `notify.failed`-style journal record + digest line, never silent. Acceptance: backup + restore round-trip test restores a journal byte-identical.

### W13 — Dogfood milestone

After P3 lands: every remaining phase (P4+) is authored as a plan in `docs/plans/` and executed BY the harness at autonomy level 1, promoting to 2 as stats allow. Every friction encountered is filed as a lesson (A7) or plan-lint rule (V3). This is the standing integration test; the plan doc for each phase records `executed_by: harness` in meta. Acceptance: P4's landing commit chain shows `plan/<slug>` integration history.

### Phase mapping (delta)

- **P2** += W8 (resolver rule + taxonomy `gate-flaky` evidence field), W6 (snapshot on quarantine — lands with quarantine path).
- **P3** += W9 (with A5), W10 (daemon launch gate), W12 (daemon timer), W5 (status cache written by daemon).
- **P4.5** += W2, W3 (share V4's journal reader), W4.
- **New P4.6 — Observability surfaces:** W1 (TUI), W7 (transcript archive + index), W11 (OTel exporter).
- **Post-P3 standing rule:** W13 dogfooding governs how P4+ themselves are executed.

## PART VIII — Orchestration, quota, and quality-of-landing improvements (accepted 2026-07-07; adapter capability matrix REJECTED — do not build, not even an `attachments` flag)

### X1 — Meta-plans (cross-plan dependencies)

`meta.depends_on: [<slug>, ...]` on a plan's meta line. Daemon maintains a queue: a plan with unmet deps sits `queued-on-deps`; when every dep's journal shows terminal-landed (run.done, zero quarantines OR an explicit `meta.deps_accept_partial: true` on the dependent), the daemon launches it through the normal preflight→launch path (preflight still gates — a queued plan with open decisions escalates instead of launching). Cycle detection across `docs/plans/*.jsonl` at queue time (reuse V3 lint's graph code). Journal records: `plan.queued {on}`, `plan.released {by}`. Surfaces: `runplan watch` portfolio header, W5 statusline count includes queued plans. Acceptance: fixture pair A→B; B launches only after A lands; A quarantine + no accept_partial → B escalates `blocked-on-dep`.

### X2 — Mid-run plan amendment (`runplan amend`)

`runplan amend <slug> <patch.jsonl>`: append-only task additions; exact splice procedure, payload (`plan.amended {patchHash, tasks: [full records]}` — journal is replay authority), duplicate-patch idempotence, and collision rules are canonical in `spec/SCHEDULER.md` § Amendment.

### X3 — Canary-first scheduling

Within a ready set, dispatch order = risk tier DESC, estimated cost ASC (risk tier already per-task via `lib/risk-router.sh`; cost estimate = static tier table until V4 stats exist). Pure ordering function in the scheduler module (V7 split), unit-tested; no config — it is strictly better than arbitrary order. Effect: a doomed plan fails in minutes instead of after siblings burn budget. Acceptance: fixture ready-set orders as specified; chaos plan's gate-red task dispatches in the first slot of its wave.

### X4 — Idle-time maintenance window

Config `maintenanceWindow: {start, end}` (local time). Daemon, when window is open AND no user runs active: runs the housekeeping ladder — `runplan gc --dry-run` (report into digest), V9 adapter smoke, W12 backup verify, soak iteration (configurable subset). Each housekeeping job is itself journaled under a reserved slug prefix `maint-` so W2/W4/stats see them. User run submitted during maintenance → maintenance job finishes its current step then yields (jobs are step-resumable by construction — they are plans). Acceptance: daemon in-window with idle machine runs the ladder; submitting a run mid-ladder preempts before the next step.

### X5 — `provider-quota` fail-class + reroute

New `FAIL_CLASSES` entry `provider-quota` (transport band, NOT task-attributed, NOT fixer-repairable). Wrapper contract addition: wrappers MUST map their CLI's quota/429/subscription-exhausted errors to a distinct event (`error.kind: "quota"`); unmapped adapters treat all transport errors generically (WARN in preflight for unattended). Resolver actions for this class only: (1) reroute the attempt to the next enabled adapter whose model tier satisfies the task's risk tier (router call), (2) if none → park task `quota-parked` + schedule retry at `retryAfter` (from the provider header when available, else config default), run continues siblings on other adapters, (3) all adapters exhausted → pause run + escalate. Stats (V4) track quota events per adapter per hour-of-day — feeds X6. Acceptance: stub wrapper emitting quota errors → task lands via reroute; all-adapters-quota fixture → run pauses with correct decision.

### X6 — Off-peak run windows

Plan/repo config `runWindow: {start, end, tz}` — `--unattended` launches submitted outside the window are queued (`plan.queued {on: "window"}`) and auto-launched at window open by the daemon. `--now` flag overrides explicitly. Rationale surfaced in the queue message: quota freshness (X5's hour-of-day stats printed when available). No auto-inference of windows — operator sets them. Acceptance: submission outside window queues; daemon clock-mock test launches at open; `--now` bypasses.

### X7 — Cross-model review diversity

Engine rule at review dispatch: reviewer adapter+model MUST differ from the implementer's for that attempt (both stage-1 spec and stage-2 quality reviews). Selection: router picks the highest-pass-rate different model of adequate tier; only one adapter enabled → WARN in preflight (`review-diversity-unavailable`), proceed (never block on it — diversity is a quality lever, not a gate), journal the degradation. Fixer attempts count as implementation — the reviewer of a fixed diff must differ from the FIXER's model. Acceptance: dispatch-selection unit test; single-adapter fixture journals the degradation WARN.

### X8 — Declared task artifacts

Optional per-task plan fields `artifacts: [path-or-glob, ...]` and `verifycmd: <cmd>`. Post-implement gate (before review): every declared glob matches ≥1 file changed-or-added on the task branch; `verifycmd` (run in the task worktree) exits 0. Failure → fail-class `artifacts-missing` (gate band — enters the normal fixer ladder; a fixer CAN legitimately fix "forgot to write the test"). V3 lint WARNs on tasks with no artifacts and no verifycmd (heuristic nudge, not error). Acceptance: chaos "exits 0 having done nothing" scenario now fails deterministically at this gate before wasting a review.

### X9 — Commit provenance trailers

Every commit the engine or its agents create gets trailers: `Plan: <slug>`, `Task: <taskId>`, `Run: <runId>`. Enforcement: engine passes `--trailer` args at its own commit sites AND the post-implement gate verifies the task branch's commits carry them (fixable class — fixer amends). Ship/land preserves them (merge commits get `Plan:`/`Run:`). Acceptance: `git log --grep 'Plan: '` over a chaos run returns every landed commit; gate catches a trailer-less commit.

### X10 — Config layering

Schema-validated merge chain, later wins: engine defaults → `~/.harness/config.json` → `<repo>/.harness.json` → plan `meta` → CLI flags. One implementation: `src/config.js` exporting `resolveConfig(cli, planMeta, repoRoot)`; EVERY config consumer (daemon, runner, router, notifier) goes through it — no direct env/file reads elsewhere (grep-enforceable rule; add to V3-adjacent repo lint). Unknown keys → error naming the layer (fail-closed, catches typos like `budgett`). Protected-path hard defaults (V1) are non-removable at every layer by construction. `runplan config --show` prints the merged result with per-key provenance (`key: value  (from repo)`) — this provenance view is the debugging tool that pays for the feature. Acceptance: table-driven merge tests incl. unknown-key rejection and provenance output.

### X11 — Escalation handoff bundle

Every `decision.requested` escalation ALSO writes `runstate/handoffs/<slug>-<decisionId>.md`: halt reason verbatim, journal tail (last N relevant records), W2 why-chain for the affected task, snapshot bundle path (W6), merged-config provenance for relevant keys (X10), and a suggested-actions section (the resolver's closed action set with its rationale for escalating). Format follows the operator's handoff-skill conventions (resumable context doc). The notification (W9) links the file path. Purpose: the interactive session opened to resolve it starts warm. Acceptance: chaos escalation produces a bundle containing the exact injected failure string and a valid why-chain.

### X12 — Run report beside the plan

On terminal state: write `docs/plans/<slug>-report.md` — outcome, landed/quarantined/skipped table, total + per-task cost (V2), timeline summary (W3 one-liner per task), decisions taken (by whom: user/default/resolver), lessons emitted (A7), `.warnignore` deltas — and commit it together with the landing (or as a follow-up commit on the integration branch for `pr` mode). The plans directory becomes intent+outcome history readable without tooling. X2's merged plan is committed back in the same step. Acceptance: chaos run yields a report whose table matches the journal summary exactly (generated from the same reducer — no second source of truth).

### X13 — Plan templates

`runplan new <slug> --template feature|refactor|audit`: scaffolds `docs/plans/<date>-<slug>.jsonl` + companion `.md` with the boilerplate pre-filled — meta (land_mode placeholder marked INVALID so preflight blocks until stamped, preset, budget keys), gated-decision skeleton, per-task `artifacts`/`verifycmd` stubs (X8), risk tiers. Templates live in `presets/templates/*.jsonl` and are validated by V3 lint in CI (a template that lints ERROR-free except for its deliberate placeholders). V3's `plan-authoring` lessons cite template fixes as their remediation. Acceptance: scaffolded plan fails preflight ONLY on its placeholders; filling them makes it lint-clean.

### Phase mapping (delta)

- **P1** += X5 taxonomy entry + wrapper-contract quota mapping (schema change travels with V2's usage event — one contract rev, not two), X9 trailers (cheap, wanted before any unattended landing), X10 config layering (foundational — A3/V1/V2 config keys all route through it; land FIRST in P1).
- **P2** += X3 (scheduler ordering fn, lands with V7 split), X8 (gate), X7 (review dispatch rule).
- **P3** += X1 meta-plans, X4 maintenance window, X6 run windows, X11 handoff bundle (with A5), X12 report (daemon terminal-state hook).
- **P4** — X2 amendment lands here (touches scheduler; simpler once DAG mode exists, but MUST work in wave mode too).
- **P4.5** += X13 templates (with V3 lint).

## PART IX — Self-improvement, closed-loop (supersedes A7's mechanism; A7's hooks remain the Observe stage)

Canonical: `spec/SELF-IMPROVEMENT.md`. Summary of the shift: A7 as originally written learns *advice for prompts* (write-only lessons, unmeasured). PART IX learns *policy for registries*, proves every learning with efficacy data, retires what does not work, and requires human approval for anything normative.

- **Y1 — Loop:** Observe (journal/stats) → Attribute (postmortem LLM call, closed category set, routes each failure's learning to the right registry) → Propose (evidence-backed `improvement-proposal` decisions) → Approve (decision inbox; HUMAN-only for normative registries, no auto-default ever) → Apply (provenance-tagged overlay entries + improvement ledger) → Measure (claimed metric vs baseline over a window) → Retire/Revert (auto-proposed on regression).
- **Y2 — Lesson lifecycle + efficacy:** injections journaled per attempt (`attempt.lessons`), Laplace-smoothed success rate vs class baseline, `candidate → validated → retired` transitions, top-3 injection cap. Kills the lesson-landfill failure mode where accumulated unproven lessons degrade fixer prompts.
- **Y3 — Deterministic layers become learnable (via HUMAN-approved overlays):** taxonomy overlay from clustered `unknown` failures (anchored, collision-checked patterns — the shrink rate of the `unknown` bucket is the headline maturity metric); resolver-row promotion when the LLM rung is consistently right (the LLM works itself out of a job per fail-class); watchdog budget tuning from duration percentiles; WARN-only lint rules; router weights (auto — pure stats artifact).
- **Y4 — `maint-self-improve`:** weekly maintenance-window plan that runs the whole loop as a normal journaled/resumable/chaos-testable plan — makes W13 dogfooding permanent rather than a milestone.
- **Never learnable (hard-coded):** `.warnignore`, review skips, protected paths, gate definitions, the resolver action set, the registry list itself.

Phase mapping: postmortem + lesson lifecycle + `attempt.lessons` land WITH A7 in **P5** (same code paths — build the measured version directly, do NOT build unmeasured A7 first and retrofit); registries/overlay loading + proposal generation + ledger + `maint-self-improve` form **new P5.6** (needs V4 stats and A5 inbox, both landed by then); chaos C2 extension per spec travels with P5.

## Non-goals

- No live WebSocket agent mesh, no external DB, no multi-user auth (single-user localhost tool).
- No rewrite of runner/journal/OKF core — extend only; DAG scheduler is flag-gated.
- No speculative adapters for CLIs not installed on this machine.
- No autonomous gate-weakening — `.warnignore` additions and review skips are never resolver actions.
- No `runplan simulate` / predictive dry-run — rejected by operator 2026-07-07. Preflight does not estimate cost/ETA.
- No adapter capability matrix — rejected by operator 2026-07-07, including any `attachments`/`vision`/`maxContext` fields. If a capability-mismatch failure ever occurs in practice, design from that evidence via the taxonomy.
