# Design: migrate `brainstorm`/`handoff` to target the harness engine by default

Slug: `harness-migration`

## 1. Context

`docs/specs/2026-07-01-run-plan-harness-gaps-design.md` (slug `run-plan-harness-gaps`) fixes and proves the harness engine itself (Gaps A-D: a real crash, plus decision-gate/isolate/risk-escalation parity with the Workflow controller). This spec is the follow-on: flip `brainstorm`'s default plan-emission format from `session-state/v1` to harness-native, and update `handoff` to handle harness-native plans correctly.

**Hard dependency, not a suggestion:** this spec does not start until `run-plan-harness-gaps` lands AND its exit criterion is met — one real plan runs end-to-end through `bin/runplan` to completion (all waves `done`, no D2-class crash on any resume). Reason: D2b (a resume-path bug where an interrupted run's recovered binding bypasses fail-closed seat resolution) fires on ANY interrupted+resumed harness-native run. Migrating means many more real runs, hence many more resumes — shipping this default flip before D2b is proven fixed means the migration itself becomes the exposure. This is a sequencing decision the user made explicitly (asked via AskUserQuestion: split into two plans vs. one gated plan vs. no gating — user chose the split).

**Scope, confirmed with user:** "the project is migrating to mega-plan-harness." Going forward, new plans default to harness-native. This is NOT a `session-state/v1` → harness-native *translation* layer — old already-authored plans are untouched and keep running on the Workflow controller (routing already discriminates by the `schema` key, done prior to this spec). Only *new* plans, from the point `brainstorm` is updated, default to harness-native.

**Audit performed (checked, not assumed) — scope of skills needing migration:** every skill in `~/.claude/skills/` was checked for `session-state/v1`/harness-format dependencies. Two need changes: `brainstorm` (Gap E) and `handoff` (Gap F). Confirmed NOT needing changes:
- `bs-cursor`/`bs-north`/`bs-multi` — target `cursor-orchestrator`/`north-orchestrator`/`multi-orchestrator`, a separate Workflow-controller-family of executors; none ever dispatch through `bin/runplan`. Orthogonal to this migration.
- `fix-rot` — recovers a rotted Workflow-controller main thread (subagent-dispatch conversational model losing context). A harness-native run is one foreground `bin/runplan` process with its own durable journal; no analogous "rotted main thread" to reconstruct.
- `ship`/`sdd`/`executing-plans` — execute directly off the markdown plan doc; none read the JSONL/schema at all.
- `run-plan` — already migrated (schema-based routing, done prior to this spec).

## 2. Gap E: `brainstorm` emits harness-native plans by default

**Verified before designing this:** `src/runner.js`'s `run()` groups tasks purely by `task.wave` (ascending, strict order) and runs each wave through `runPool(tasks, concurrency, ...)` — no `deps` field, no `meta.scheduler` concept exists in the harness engine at all (grep confirmed zero references). `buildTaskPrompt` builds the dispatch prompt from `task.desc` ALONE — no anchor/plan-doc auto-read exists in `src/runner.js`, unlike the Workflow-controller's dispatched subagents (which independently read the plan doc themselves). This means a harness-native `task.desc` must be a self-sufficient dispatch instruction, not the short label session-state/v1 tasks get away with.

**Verified, not assumed — the harness engine does not isolate tasks into git worktrees.** Checked because a worktree cut from a branch would miss an uncommitted plan `.md`, breaking a pointer-style `desc`. It does not: `runWrapper` (`src/runner.js:428-435`) passes `--workspace <repoRoot>` directly; `context.worktree` is set to `context.repoRoot` verbatim at `src/runner.js:20` and never reassigned; no `git worktree add` call exists anywhere in `src/runner.js`, and grepping the whole repo confirms it. The dispatched CLI always runs in the live working tree, so an uncommitted `docs/plans/<slug>.md` is visible to it.

Also confirmed the source of the `/tmp/wt-<slug>-<task>` directories seen on disk (they predated this check and could have looked like harness worktrees): `~/.claude/workflows/lib/run-plan-lib.sh:421-449` creates them — that's the Workflow controller's OWN per-task worktree mechanism, entirely separate from `bin/runplan`. A harness-native (no-`schema`) plan never reaches `run-plan-lib.sh` at all (routing already discriminates on the `schema` key), so this mechanism cannot apply to harness-native runs. The pointer design below is safe as designed.

### Schema (harness-native session file, brainstorm-authored)

```jsonl
{"type":"meta","slug":"<PLAN_SLUG>","base_branch":"<branch>","gate0_mode":"strict","preset":"<preset — see rule below>"}
{"type":"gated", ...}                     // optional, exact shape from run-plan-harness-gaps §3 (Gap A)
{"type":"task","id":"t<N>","wave":<N>,"seat":"coder","tier":"medium","desc":"<see rule below>","requires_decision":null}
```

No `schema` key on the `meta` line (that absence IS the discriminator `run-plan` already uses). No `exec_mode`/`scheduler` fields — irrelevant to this engine. `goal`/`methodology`/`intent`/`direction`/`anchor`/`session_memory` records (brainstorm's existing durable-header vocabulary) are UNCHANGED — those are prose-context records `run-plan`'s Harness engine path never reads; they exist purely for a human/agent re-reading the file, same as today.

**Field rules:**
- `id`: `"t<N>"`, `N` = the plan doc's Task number. Stable, collision-free within one plan.
- `wave`: copied directly from the plan doc's Wave Plan table — the SAME topological-sort output `plan` already produces; no re-analysis.
- `seat`: always `"coder"`. One task record = one unit of work; `reviewer`/`fixer` seats are resolved automatically per-task at RUN time by `runTask`/`resolveOptionalSeat` (already true today), never authored per-task.
- `tier`: preset seat-tier key, closed enum `low`/`medium`/`high` (`spec/presets.schema.json`, `lib/resolve-seat.sh`) — this selects the CODER's model/wrapper binding, default `"medium"` unless the plan deliberately wants a cheaper/stronger model for that task. Not a risk/importance field: risk-based review escalation (run-plan-harness-gaps Gap C) is a separate mechanism keyed off `riskLevel` from the diff, and only adds a REVIEWER seat — it never rewrites `task.tier`.
- `preset`: the preset the harness engine path uses (`meta.preset` per the existing Harness engine path SKILL.md step 2 rule) — brainstorm asks once (or reuses the project's already-established default) same as it decides `land_mode` today via `ship-init.sh`.
- `desc`: **a pointer, not a body** — DRY with the plan `.md`, same doctrine as spec-writing (Contract-level spec). Exact required shape: `"Read docs/plans/<slug>.md Task <N> (<Component Name>) for the full contract. <one-line behavior summary>. Acceptance: <verbatim acceptance command from the plan doc>. Commit only: <exact Files list paths>."` This keeps the JSONL from duplicating contract prose the plan `.md` already owns, while staying self-sufficient for `buildTaskPrompt` (no other file gets auto-read by the harness engine).
- `requires_decision`: `null` unless the decision-enumeration pass linked a `gated` record to this task.

### Where this is authored

Brainstorm's checklist step 8 ("populate section 9 from the wave/phase table") is the exact point this branches. Add ONE explicit trigger there: **"target engine: harness-native (default) unless the user has explicitly said this run needs Workflow-controller/subagent-dispatch execution — no per-run question, no content-based heuristic."** Everything upstream (wave planning, decision-enumeration, file-structure analysis) is IDENTICAL between both emission targets — only the final JSONL serialization forks.

**Concrete mechanism (pinned, not left to judgment):** the session file schema already has a slot for exactly this — `{"type":"direction","verbatim":"<explicit standing rule, quoted EXACTLY>"}`. If, anywhere earlier in the brainstorm conversation, the user said something requiring Workflow-controller/subagent-dispatch (e.g. "dispatch each task to its own subagent", "use the orchestrator"), that statement is ALREADY captured verbatim as a `direction` record (existing brainstorm behavior, unchanged). Step 8's branch is a lookup, not a new judgment call: scan already-written `direction` records for one implying Workflow-controller need → session-state/v1; none found → harness-native (default). No new prompt, no new record type, no new question to the user.

`/plan` itself is UNCHANGED — it never wrote the JSONL (that has always been brainstorm's "Compact-Proof Session File" step). A standalone `/plan` invocation (no brainstorm) still produces only the `.md` and hands off to `ship`/`executing-plans` — untouched, since neither ever reads the JSONL.

## 3. Gap F: `handoff` gains a harness-native branch

`handoff`'s "Session-state JSONL" procedure (its § 2) assumes `session-state/v1` unconditionally: it sets `meta.exec_mode` and reconciles per-task `status` fields against git. Neither concept exists in harness-native plans — there is no `exec_mode` (no orchestrator to select; `bin/runplan` is invoked directly) and no per-task `status` in the plan file at all (task progress lives in the harness's OWN journal, `runstate/<slug>.jsonl`, already the resume source of truth for `bin/runplan --resume`).

**Fix:** before running its existing procedure, `handoff` reads the JSONL's first line and branches:
- No `schema` key (harness-native) → skip the exec_mode/task-status reconciliation entirely. The harness engine's own journal already tracks progress; nothing in the plan JSONL needs rewriting. Surface to the user: `"Session slug: <slug> — resume with bin/runplan <slug> --preset <preset> --resume (harness engine; the runstate journal is the resume source of truth, not this plan file)."`
- `"schema":"session-state/v1"` → existing procedure, unchanged.

No schema change to harness-native plans needed for this gap — `handoff` just needs to stop assuming every plan file it touches is `session-state/v1`.

## 4. Testing strategy

- **E:** manual/documentation-level (SKILL.md prose change; brainstorm authors JSONL by hand, no new automated tooling to unit-test) — verify by running `brainstorm` end-to-end on a small throwaway topic, confirming the emitted JSONL matches §2's schema, and feeding it straight into `bin/runplan` to confirm it dispatches cleanly (post-`run-plan-harness-gaps`, so no D2-class errors expected).
- **F:** manual/documentation-level — verify by invoking `handoff` against a harness-native plan mid-run and confirming it takes the new branch (no `exec_mode` write attempted, no task-status rewrite attempted, correct resume message surfaced).

## 5. Architecture Decisions

- **`desc` is a pointer into the plan `.md`, not an inlined contract** — collapsed candidate: duplicate the full contract text into `task.desc` so the CLI-wrapped coding agent needs no extra read. Rejected: duplicates content the `.md` already owns (drifts on edit), and `wrappers/*.sh` already pass `--workspace` (full repo access, confirmed not worktree-isolated — see §2 above) to the underlying coding CLI, which can open the `.md` itself same as a Workflow-controller subagent does today.
- **`handoff`'s harness-native branch does no jsonl rewriting at all** — collapsed candidate: mirror session-state/v1's reconciliation (rewrite task statuses, stamp something into meta). Rejected: the harness's own `runstate/<slug>.jsonl` journal is already durable resume ground truth; duplicating that into the plan file would create two sources of truth that can drift.
- **Gated on a proven harness engine rather than shipped alongside it** — collapsed candidate: one combined plan (A-D + E/F). Rejected on user's explicit choice: D2b is a resume bug a migration would trigger constantly; proving A-D first (own plan, own exit criterion) before flipping the default producer is the safer sequencing.
