# mega-plan-harness — Design

audience: AI coding agents first. Contract-level: seams + decisions, NOT code bodies.
slug: `mega-plan-harness` · date: 2026-06-27

## Purpose

A provider-agnostic plan-execution harness. Anthropic models (or any planner) author a portable DAG; the
harness executes it by binding each **seat** (coder/reviewer/fixer) to a configurable **engine**
(north/cursor-composer/codex/grok/…) per run. Two control planes share ONE work core: a **standalone Node
runner** (anthropic-less, runs anywhere — opencode/codex/headless/cron) and a **DynWF driver** (a Claude
Code `Workflow` script). This repo owns the shared core and BOTH planes; `~/.claude` becomes a thin caller.

Canonical contracts already authored — READ THESE FIRST, do not duplicate them here:
`spec/WRAPPER-CONTRACT.md`, `spec/PRESETS.md`, `spec/FORK.md`, `spec/presets.schema.json`,
`spec/runconfig.schema.json`. This design pins the BUILD: components, seams, sequencing, tests.

## Non-goals (YAGNI)

- No UI this run (separate later project; the runconfig schema is its eventual contract).
- No new engines beyond north/cursor/codex wrappers (grok later — additive, one wrapper).
- No grammar for the plan beyond the existing `session-state/v1` JSONL.

## Architecture (summary — full fork rules in `spec/FORK.md`)

```
        plan (session-state/v1 JSONL)  +  run-config ({preset, overrides})
                                  │
                    ┌─────────────┴─────────────┐
           DynWF driver                   standalone runner
        (src/dynwf-driver.js)              (src/runner.js)
        Workflow script,                   Node CLI, no model,
        agent(haiku)→Bash→core             spawn→core
                    └─────────────┬─────────────┘
                          SHARED CORE (lib/ + wrappers/)
   resolve-seat · dispatch(wrapper) · gates(gate0,risk) · journal(JSONL+git) · lease/commit
```

Iron rule (from FORK.md): a control plane contains ONLY control flow (sequence/concurrency/resume/journal).
Every unit of WORK is a shared CLI both planes invoke identically. State of record = git + JSONL, never the
Workflow cache.

## Components & seams

### lib/resolve-seat.sh  (shared core — CLI, NOT inline JS)

Contract (verbatim from PRESETS.md resolution ladder):
```
resolve-seat.sh --plan <jsonl> --runconfig <json> --node <id>
  → stdout: binding JSON {seat,tier,wrapper,model,timeout?,flags?}  exit 0
  → unknown seat/tier OR unresolvable binding → exit 2, stderr {ok:false,detail}  (fail-closed)
```
Resolution order: runconfig.overrides["seat.tier"] → preset.seats[seat][tier] → preset.seats[seat] (flat)
→ exit 2. Fallback on dispatch-failure is the RUNNER's concern (it owns retry), not this resolver. Pure
function of (plan,runconfig,node). Deterministic gates (gate0,risk) are NOT seats — resolver rejects them.

### wrappers/  (shared core — each satisfies spec/WRAPPER-CONTRACT.md)

- `wrappers/codex.sh` — VENDOR + adapt from the EXISTING `~/.claude/workflows/lib/cx.sh` (codex gpt-5.5
  wrapper, already in use). NOT a from-scratch build. Confirm/adapt to the contract: `--workspace --trust
  --task-slug [--model] [--timeout]` → exit 0/124/2/3; foreground `timeout -k 5`, stdin </dev/null, env
  scoped to subprocess, model pinned, raw log to file. Verify cx.sh's rc semantics emit 0/124/2/3 distinctly.
- `wrappers/na.sh`, `wrappers/ca.sh` — VENDORED from `~/.claude/skills/{north,cursor}-orchestrator/`.
  Copy verbatim, confirm each already satisfies the contract (na.sh does; ca.sh: verify rc semantics —
  it currently `set -e`s and may not emit 124/3 distinctly → adapt to the contract's exit codes).
- All wrappers: a `wrappers/_contract-probe.sh <wrapper>` smoke test asserts the flag set + exit-code map.
- **Preset wrapper paths MUST be repo-relative `wrappers/<x>.sh`** — the canonical copies live HERE. The
  `~/.claude/skills/...` paths in PRESETS.md examples are illustrative of "today's behavior"; the shipped
  presets in `presets/` bind the vendored `wrappers/` copies. `presets/_validate.mjs` MUST assert every
  bound wrapper path resolves to an existing executable (no vapor — `opus-review.sh` does NOT exist; the
  `anthropic` preset's reviewer is illustrative-only and OUT of this anthropic-less plan's shipped presets).

### lib/gates.sh  (shared core — deterministic, NOT model-bound, NEVER a preset seat)

- `gate0` — run the target repo's own checks against a diff: install, typecheck, lint, build, test. Exit 0
  green / nonzero with captured failure. Repo-agnostic: read the check commands from the target repo
  (package.json scripts / Makefile), fail-closed if none found.
  - **Two declared MODES (from plan `meta.gate0_mode`, default `strict`):**
    - `strict` — absolute green (whole suite passes). Correct for feature plans on an already-green repo.
      A green-repo plan MUST use this; never silently relax it.
    - `baseline-ratchet` — for red-baseline remediation plans (suite starts red, goes green over many
      waves). Capture the failing set at run start **by stable test id** (full test name, NEVER count);
      protected-pass set = currently-passing tests. Per-task gate passes iff **(a)** no protected-pass
      test regresses AND **(b)** the task's declared `fix_targets` (baseline failures it owns) now pass.
      **Ratchet:** after each green wave, newly-passing tests join the protected set; the baseline set only
      shrinks — an already-fixed test re-breaking is a REGRESSION, not an excused known-failure.
    - Deleted/renamed tests: reconcile by id — a vanished baseline entry counts as REMOVED (e.g. a
      dead-scaffolding delete task), never as fixed or regressed. Raw set-diff is wrong when the plan edits
      tests; identity must be the test name, with rename = disappeared+new reconciled, not double-counted.
  - `intGate0` — the same check + same mode semantics run cumulatively on the integration branch after a
    parallel wave; the ratchet's protected set is the cross-wave accumulator.
- `risk` — VENDOR `~/.claude/skills/multi-orchestrator/risk-router.sh` (deterministic HIGH/LOW + money
  sub-scan + TRUST_BOUNDARY). Contract unchanged: `risk-router.sh <worktree> <base..head>` → RISK= lines.

### lib/journal.sh  (shared core — state of record)

Append-only JSONL `runstate/v1` (schema in FORK.md). Seams:
```
journal.sh append <jsonl> <record-json>          # one line, never pretty-print
journal.sh state  <jsonl> <task-id>              # → last state for a task (or "" )
journal.sh reconcile <jsonl> <repoRoot> <branch> # re-derive done-ness from git; downgrade phantom COMMITTED
```
States: `leased → implemented → gated → [reviewed] → committed`. `reviewed` conditional on a bound reviewer
seat. Resume = advance to next APPLICABLE state per run-config, never next ordinal (FORK.md).

### src/runner.js  (standalone control plane — Node CLI, ZERO Anthropic dep)

```
harness run --plan <jsonl> --runconfig <json> [--concurrency N] [--resume]
```
Behavior (control flow only — all WORK delegates to lib/ + wrappers/ via child_process):
- Parse plan; group tasks by wave; within a wave run a bounded Promise pool (default min(cores-2, 8)).
- Per task: lease → resolve-seat → spawn wrapper → compute base/head shas itself → gate0 (retry-fix loop,
  bounded) → risk → if reviewer seat bound: dispatch review seat → commit. Journal each transition.
- Fail-closed: any wrapper exit 2/3, gate not green after N, or resolver error → halt that task, record
  blocker, never silently proceed. Fallback chain on exit 3 / repeated 124 per binding (bounded 4 hops,
  cycle-detected — PRESETS.md).
- `--resume`: journal.reconcile then continue each task from its next applicable state.
- NO model anywhere in this file. Judgment steps (reconcile) are deterministic lib calls.

Seam note: runner imports lib/*.sh via child_process, NOT a JS port — single source of truth is the shell.

### src/dynwf-driver.js  (DynWF control plane — Workflow script)

The `Workflow`-tool script form of the SAME loop. Differences (allowed drift only, FORK.md):
- Plan arrives via `args` injection (no FS in script body), not fs-read.
- Dispatch = `agent(prompt,{model:'haiku',schema})` whose prompt shells out to resolve-seat.sh + the wrapper
  (haiku = pure dispatch+transcribe; never judgment). Concurrency/resume via Workflow `parallel`/runId.
- Journals to the SAME `runstate/v1` JSONL at the SAME per-task granularity, so a run is plane-portable.
- Re-derives in-flight state from git+JSONL on compaction (mirror existing orphan-workflow hook).

### presets/  (shared config — validate against spec/presets.schema.json)

`presets/anthropic.json`, `presets/anthropic-less.json`, `presets/codex.json` (worked examples already in
PRESETS.md). Each `preset/v1`. A `presets/_validate.mjs` asserts every preset + the example runconfigs
against the JSON schemas (ajv-free: hand-rolled validator, zero deps per repo policy).

### ~/.claude integration — Plan B, a SEPARATE run (NOT this plan)

`~/.claude/workflows/run-plan.js` becomes a THIN caller of `src/dynwf-driver.js`; the hardcoded
`executor: enum['north','cursor']` + `P.implementNorth`/`P.implement` fork is REPLACED by seat+tier
resolution via resolve-seat.sh. This work is OUT OF SCOPE for this plan and lives in a separate Plan B.

Two hard reasons it cannot be a wave of this plan:
1. **Single repoRoot.** run-plan binds ONE `m.repoRoot` per run (verified: setup/lease/commit/finish-branch
   all use `m.repoRoot`, one integration branch from one `base_branch`). Tasks touching `~/.claude` AND
   `mega-plan-harness` are not expressible in one run.
2. **Self-edit hazard.** This build is DRIVEN by the existing `~/.claude/run-plan.js`; Plan B MODIFIES that
   same file. The executor editing itself mid-run is a footgun (resume reloads a changed driver), and
   touching daily-driver tooling is the confirm-first / hard-to-reverse class → deliberate separate launch.

Plan B (later run, `repoRoot=~/.claude`): replace run-plan.js with the thin shim once Plan A's runner
integration test passes; keep the old path working until then (no big-bang cutover).

## Data flow

planner → JSONL plan (engine-agnostic seats) → operator picks run-config(preset) → control plane loops:
resolve-seat → wrapper(engine) → gate0 → risk → [review] → commit, journaling each step to git+JSONL.

**Plan node vocabulary = `seat` + `tier`, NEVER an engine id.** Each `task` record carries `seat` (e.g.
`coder`) + `tier` (`low|medium|high`, planner's difficulty call). The preset maps `tier→engine`.
NEVER a `task.executor:north|cursor` field — that bakes an engine into the portable plan (PRESETS.md). The
old `run-plan.js` runtime `classify` step (sonnet picks executor per task) is REPLACED: the Anthropic
planner pins `tier` at author time, so NEITHER plane runs a classify model. This is what makes the
standalone runner truly anthropic-less.

**Trade-off (state it, don't hide it):** plan-time tiering buys out runtime classification, losing runtime
adaptivity (stale plan / drifted repo / mis-pinned tier). That loss is NOT recovered by re-classifying — it
is caught by the deterministic floor (`gate0`+`risk`, run unconditionally, never seats), the operator's
`runconfig.overrides` at launch, and `fallback` on engine-down. Do NOT add a DynWF-only classify model "for
adaptivity" — a step in one plane only is a FORK drift bug. Tier refinement, if ever wanted, is an OPTIONAL
pre-plan authoring enrichment both planes consume, never an execution-time step.

## Error handling

Fail-closed everywhere (repo-wide invariant). Unknown config, missing wrapper, engine-down (exit 3), gate
red after bounded retries, resolver error → HALT with recorded blocker. NEVER substitute an engine except
via an explicit `fallback` chain. An erroring deterministic gate reads as RED, never green.

## Testing strategy

- Per shared-core unit: a shell test (`test/*.sh`) asserting both branches (success + fail-closed). Models:
  `~/.claude/workflows/lib/test-*.sh`.
- `wrappers/_contract-probe.sh` against each wrapper (mock engine) — asserts flag set + exit-code map.
- `presets/_validate.mjs` — schema conformance of every preset + example runconfig.
- Runner integration test: a fixture plan + a stub wrapper (echoes a diff) → assert journal transitions
  `leased→…→committed` and a fail-closed halt on stub exit 3.
- DynWF driver: dry-run that asserts it shells the SAME resolve-seat.sh + wrapper paths as the runner
  (drift guard — the two planes must call identical core CLIs).

## Bootstrap

Built using the EXISTING ~/.claude run-plan/cursor tooling (chicken-and-egg resolved: the old harness builds
the new one). Engine for the build itself = cursor/composer per task class; opus reviews. The new harness
does not execute itself until its runner integration test passes.

## Architecture Decisions

- **Shared core is shell CLIs, not a JS lib.** Deletion test: a JS port would have to exist twice (DynWF
  script can't `require`) → drift. Shell CLI invoked by both planes = single source of truth. Deep seam.
- **resolve-seat as its own CLI** (not inlined): same reason — survives the no-`require` constraint. Deep.
- **Wrappers vendored, not referenced cross-repo at build time:** new repo owns canonical wrappers;
  `~/.claude` skills converge to them in the final wave. Temporary na.sh/ca.sh duplication accepted to keep
  the build single-repo until the last cross-repo task. Medium.
- **DynWF driver lives here, ~/.claude shims to it:** single source of truth for both planes; avoids
  cross-repo integration-branch execution for everything except the one final shim task. Deep.
- **Rejected — port the loop to JS and have DynWF call it:** Workflow script body has no `require`/FS;
  impossible. Rejected on a hard runtime constraint, not preference.
- **Binding `model` = the ENGINE model, pinned IDENTICALLY by both planes.** The runner does NOT "ignore"
  it — an unpinned engine silently runs the wrong model while the log lies (the ca.sh regression,
  WRAPPER-CONTRACT). Both planes pass `binding.model` to the wrapper. The ONLY model that drifts is the
  DynWF dispatch AGENT's own `model:'haiku'` — that lives in the ~10-line dispatch glue (FORK.md allowed
  drift), is NOT in the binding, and has no runner equivalent (runner `spawn`s the wrapper directly). Deep.
- **`tier:low→north` (engine) is a DIFFERENT layer from `haiku` (DynWF dispatch agent).** The "low
  coder" seat binds the free north engine; in the DynWF plane an `agent(model:'haiku')` shells out to
  na.sh and transcribes the rc (pure dispatch+transcribe, never judgment). Do NOT conflate "haiku" with the
  seat — that is a category error that would misresolve bindings. The runner has no dispatch agent at all.
- **run-plan.js sonnet→haiku declassify is Plan B, NOT this plan.** Editing the live daily-driver
  (`~/.claude/run-plan.js`) executor/classify is the separate-repoRoot + self-edit-hazard class already
  carved out as Plan B. This plan ships the anthropic-less runner + a FRESH DynWF driver born with haiku
  dispatch glue; the existing run-plan.js is untouched until the Plan B cutover. The haiku cost-win arrives
  via the new driver, not a retrofit. Medium.
