# harness-engine-foundation 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.

**Goal:** Generalize the mega-plan-harness engine (`src/runner.js`) to run against any repo via `--repo`, fix the undocumented `--preset` gap, and add the central-install versioning/release scaffolding (`bin/harness-release.sh`, `bin/harness-init.sh`) so a versioned engine bundle can be launched from anywhere without breaking in-flight runs.

**Architecture:** One cohesive change to `src/runner.js` (new `--repo`/`--preset`/`--engine-version` flags, `ensureWebUi` fixed to resolve via `shellPath` instead of `repoRoot`) plus two new standalone shell scripts that implement the central-install layout described in the spec (`~/.harness/engine/versions/<X.Y.Z>/{src,lib,wrappers,presets,spec,bin,web}` self-contained bundles, `CURRENT` pointer file). No changes to `run-plan.js` or any skill doc.

**Tech Stack:** Node.js (runner.js, existing style — CommonJS, no framework), bash (existing script style — `set -uo pipefail`, matches `test/runner-integration.sh` / `~/.claude/workflows/lib/ship-init.sh` conventions).

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|----------------|----------------------|
| 1 | Task 1, Task 2, Task 3 | `src/runner.js`; `bin/harness-release.sh`, `VERSION`; `bin/harness-init.sh` | ✅ no overlap |
| 2 | Task 4, Task 5 | `test/runner-integration.sh`; `test/harness-release.sh`, `run-tests.sh` | ✅ no overlap |
| 3 | Task 6 | `test/ensure-web-central.sh`, `run-tests.sh` | single task |

Task 5 and Task 6 both register a new test in `run-tests.sh` — kept in separate waves (5 then 6) to avoid a same-file conflict; Task 6 additionally depends on Task 5 for this reason, not just on Task 1.

`meta.scheduler`: `dag-parallel` (wave 1 has 3 independent file-disjoint tasks).

## Decision-Enumeration Pass

Reviewed every task against baseline categories (`irreversible | fork | input | policy | architecture`) — no project `## Pre-flight gate policy` section exists in this repo's `CLAUDE.md`. No task requires a human decision: version numbering (start at `0.1.0`), central-install layout, and the `CURRENT`-pointer release mechanism are all already resolved in the spec (confirmed with the user during brainstorming, including two `advisor()`-driven reversals). No `gated` records authored.

---

### Task 1: Generalize `src/runner.js` — `--repo`, `--preset`/`--engine-version`, `ensureWebUi` fix

**Wave:** 1
**Blocks:** Task 4, Task 6
**Blocked by:** —

**Files:**
- Modify: `src/runner.js:15-65` (`main`) — resolve `repoRoot` from `args.repo`; synthesize runconfig from `args.preset`
- Modify: `src/runner.js:670-710` (`parseArgs`) — add `--repo`, `--preset`, `--engine-version` flags
- Modify: `src/runner.js:1025-1043` (`ensureWebUi`) — resolve launch script via `shellPath`, not `repoRoot`

**Contract (pin EXACTLY):**
- `parseArgs(argv)` return shape gains three optional fields alongside the existing `{planPath, runconfigPath, concurrency, resume}`: `repo` (string, undefined if `--repo` absent), `preset` (string, undefined if `--preset` absent), `engineVersion` (string, undefined if `--engine-version` absent — accepted, never read elsewhere).
- New flag parsing (same `switch` in the existing `for` loop, same pattern as `--plan`/`--runconfig`):
  - `--repo <path>` → `repo = argv[++index] || ""`
  - `--preset <name>` → `preset = argv[++index] || ""`
  - `--engine-version <X.Y.Z>` → `engineVersion = argv[++index] || ""`
- Validation (after the loop, before existing `if (!planPath || !runconfigPath)` check):
  - `preset` set AND `runconfigPath` set → `throw new Error("--preset and --runconfig are mutually exclusive")`.
  - Replace the existing required-runconfig check with: plan is always required; runconfig is satisfied by EITHER `runconfigPath` OR `preset` (not neither) — same `Usage:` error message as today when both are missing, plus the flags in the usage string: `"Usage: harness run --plan <jsonl> (--runconfig <json> | --preset <name>) [--concurrency N] [--resume] [--repo <path>] [--engine-version <X.Y.Z>]"`.
- `main()`:
  - `const repoRoot = process.cwd();` → `const repoRoot = args.repo ? path.resolve(args.repo) : process.cwd();`.
  - `const runconfig = loadJson(args.runconfigPath, "runconfig");` → when `args.preset` is set, build `{ version: "runconfig/v1", preset: args.preset }` in memory instead of calling `loadJson`; when `args.runconfigPath` is set, unchanged (`loadJson` call as today).
- `ensureWebUi(repoRoot)` — keep the exported signature `ensureWebUi(repoRoot)` unchanged (callers/tests still pass `repoRoot`), but inside the function replace `path.join(repoRoot, "bin", "ensure-web.sh")` with `shellPath("bin", "ensure-web.sh")`.

**Behavior:**
- `--repo` absent → byte-identical behavior to today (`process.cwd()`); `test/runner-integration.sh`'s existing cases must keep passing unmodified.
- `--repo <path>` present → every existing `repoRoot`-relative operation (worktrees, journal, branches — all already flow from the single `repoRoot` const) now targets `<path>` instead of cwd; `shellPath`-relative operations (`lib/*.sh` calls) are unaffected by `--repo` (they were already `__dirname`-relative).
- `--preset foo` with no `--runconfig` → runner proceeds exactly as if `{"version":"runconfig/v1","preset":"foo"}` had been loaded from a file via `--runconfig`; every downstream consumer of `runconfig` (`resolveTaskSeat`, `resolveSyntheticSeat`, etc.) is unaffected since the shape is identical.
- `--preset` + `--runconfig` together → `parseArgs` throws before `main()` does anything else; the existing top-level `try/catch` in `main()` formats it and sets `process.exitCode = 1` (no new error-handling path needed).
- `--engine-version` → parsed, stored on the returned object, never read by any other function in `runner.js`. Must NOT hit the `default: throw new Error(\`unknown argument: ${value}\`)` branch.
- `ensureWebUi` no longer requires `<repoRoot>/bin/ensure-web.sh` to exist — it now always resolves the script that ships next to the engine file itself (`shellPath("bin", "ensure-web.sh")`), matching every other `lib/*.sh` call in the file. Behavior when that script is missing/fails is unchanged (existing try/catch swallow → falls back to `"http://localhost:4321"`).

**Acceptance:**
- Run: `node -e "const {parseArgs}=require('./src/runner.js'); try{parseArgs(['run','--plan','x.jsonl','--preset','codex','--runconfig','y.json']); console.log('FAIL: no throw')}catch(e){console.log(e.message)}"`
- Expected: prints `--preset and --runconfig are mutually exclusive`
- Run: `node -e "const {parseArgs}=require('./src/runner.js'); console.log(JSON.stringify(parseArgs(['run','--plan','x.jsonl','--preset','codex','--engine-version','0.1.1'])))"`
- Expected: JSON containing `"preset":"codex"` and `"engineVersion":"0.1.1"`, no thrown error
- Run: `bash test/runner-integration.sh` (existing suite) → all existing cases still PASS (no regression from the `repoRoot` change)

- [ ] Write the two smoke checks above as part of `test/runner-integration.sh` extension (deferred to Task 4) — for THIS task, run them ad hoc to confirm before committing
- [ ] Implement the `parseArgs`/`main`/`ensureWebUi` changes per the contract above
- [ ] Run acceptance checks → expected output above
- [ ] Run `bash test/runner-integration.sh` → confirm no regression
- [ ] Commit: `git add src/runner.js && git commit -m "feat: generalize runner.js to --repo, --preset, and shellPath-relative web UI"`

---

### Task 2: `bin/harness-release.sh` — `bump` and `release` subcommands + initial `VERSION`

**Wave:** 1
**Blocks:** Task 5
**Blocked by:** —

**Files:**
- Create: `VERSION` — repo-root file, initial content `0.1.0` (no trailing newline beyond a single `\n`)
- Create: `bin/harness-release.sh` — release tooling for the central install

**Contract (pin EXACTLY):**
- Invocation: `bin/harness-release.sh bump` and `bin/harness-release.sh release [<version>]`. Unknown subcommand or no args → usage error to stderr, exit 2.
- `$HARNESS_HOME` resolution: `"${HARNESS_HOME:-${HOME}/.harness}"` (same convention as `bin/ensure-web.sh`).
- Bundle layout produced by `bump`, rooted at `$HARNESS_HOME/engine/versions/<new-version>/`:
  - `src/runner.js` (copy of repo's `src/runner.js`)
  - `lib/` (copy of repo's `lib/`)
  - `wrappers/` (copy of repo's `wrappers/`)
  - `presets/` (copy of repo's `presets/`)
  - `spec/` (copy of repo's `spec/*.schema.json` — schema files only, not the whole `spec/` dir if it holds other doc types; check `spec/` contents at implementation time and copy the schema files `*.schema.json`)
  - `bin/ensure-web.sh` (copy, single file — NOT the whole `bin/` dir, to avoid pulling in `runplan`/`harness-release.sh`/`harness-init.sh` into the bundle)
  - `mega-plan-harness/web/dist` (built web UI — mirrors the existing repo-relative path `bin/ensure-web.sh` resolves via `WEB_DIR="${REPO_ROOT}/mega-plan-harness/web"`; the bundle MUST preserve this exact relative nesting so the bundled `ensure-web.sh` resolves its `WEB_DIR` unchanged once `REPO_ROOT` becomes the bundle's own `versions/<v>/` dir)
- `bump` subcommand body:
  1. Read `VERSION` at repo root; parse as `MAJOR.MINOR.PATCH`.
  2. Increment `PATCH` by 1 only. If invoked with any argument (`bin/harness-release.sh bump <anything>`), refuse — fail closed, exit 3, stderr message stating minor/major bumps require manually editing `VERSION` first.
  3. Create `$HARNESS_HOME/engine/versions/<new-version>/` with the layout above (`mkdir -p` + `cp -r` per subtree).
  4. Overwrite repo-root `VERSION` with the new version string (`\n`-terminated).
  5. Print the new version to stdout on success.
- `release [<version>]` subcommand body:
  1. If `<version>` omitted, use the current repo-root `VERSION` content.
  2. Verify `$HARNESS_HOME/engine/versions/<version>/` exists — if not, fail closed, exit 4, stderr message naming the missing bundle dir.
  3. Write `<version>` (exact string, `\n`-terminated) to `$HARNESS_HOME/engine/CURRENT`, overwriting any existing content.
  4. Print `<version>` to stdout on success.

**Behavior:**
- `bump` NEVER touches `$HARNESS_HOME/engine/CURRENT` — a bumped version is not live until `release` runs.
- `bump` NEVER deletes or overwrites an existing `versions/<version>/` dir — if the target dir already exists (re-running `bump` without a `VERSION` change), fail closed rather than silently clobbering a version other runs may be pinned to.
- `release` NEVER copies/mutates anything under `versions/` — it only ever writes the single `CURRENT` file.
- Both subcommands are idempotent to re-run given the same inputs, except `bump`'s dir-exists guard above.

**Acceptance:**
- Run (from a scratch `HARNESS_HOME`): `HARNESS_HOME=$(mktemp -d) bash bin/harness-release.sh bump` then `HARNESS_HOME=<same dir> bash bin/harness-release.sh release`
- Expected: `versions/0.1.1/src/runner.js` exists (byte-identical to repo's `src/runner.js` at bump time) and `versions/0.1.1/lib/resolve-seat.sh` exists; `CURRENT` contains `0.1.1`; repo-root `VERSION` now reads `0.1.1`
- Run: `HARNESS_HOME=$(mktemp -d) bash bin/harness-release.sh bump 0.2.0` (an argument passed)
- Expected: exit 3, stderr names minor/major requiring manual `VERSION` edit; no `versions/` dir created

- [ ] Write `VERSION` with initial content `0.1.0`
- [ ] Implement `bin/harness-release.sh` per the contract above, `chmod +x`
- [ ] Run acceptance checks → expected output above
- [ ] Commit: `git add VERSION bin/harness-release.sh && git commit -m "feat: add bin/harness-release.sh (bump/release central-install versioning)"`

---

### Task 3: `bin/harness-init.sh` — per-repo thin wrapper generator

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

**Files:**
- Create: `bin/harness-init.sh`

**Contract (pin EXACTLY):**
- Invocation: `bin/harness-init.sh <repoRoot>`. `<repoRoot>` must be an existing directory containing `.git` — else fail closed, exit 3, matching the validation style of `~/.claude/workflows/lib/ship-init.sh` (`[[ -e "$ROOT/.git" ]] || die "not a git repository: $ROOT"`).
- Materializes `<repoRoot>/.claude/scripts/runplan` with this exact body (the `$HARNESS_HOME` default and `<repoRoot>` value baked in literally, absolute path, no runtime env dependency other than an optional `HARNESS_HOME` override):
  ```bash
  #!/usr/bin/env bash
  set -euo pipefail
  HARNESS_HOME="${HARNESS_HOME:-<absolute home>/.harness}"
  CURRENT="$(cat "$HARNESS_HOME/engine/CURRENT")"
  exec node "$HARNESS_HOME/engine/versions/${CURRENT}/src/runner.js" run --repo "<repoRoot>" "$@"
  ```
  (`<absolute home>` = the invoking user's `$HOME` at generation time, matching the `${HOME}/.harness` default used elsewhere; `<repoRoot>` = the absolute, resolved argument.)
- `chmod +x` the generated wrapper.
- Registers `.claude/scripts/` in `<repoRoot>/.git/info/exclude` if not already present (same mechanism `ship-init.sh` uses — append the line `.claude/scripts/` if a grep for it comes up empty; do not duplicate if already present from a prior `ship.sh` init).
- Prints the wrapper's absolute path to stdout on success.

**Behavior:**
- Idempotent — re-running regenerates the wrapper unconditionally (this is the "thin, regenerate freely" wrapper, unlike the ship-method wrapper which is drift-checked; `CURRENT` is read fresh at every wrapper INVOCATION, not at generation time, so regenerating the wrapper itself is cheap and always safe).
- `<repoRoot>/.claude/scripts/runplan` invoked with no `$HARNESS_HOME/engine/CURRENT` file present → the generated wrapper's `cat` fails per bash's `set -euo pipefail`, producing a clear non-zero exit and stderr from `cat` itself — no special-casing needed in the generator.

**Acceptance:**
- Run: `bash bin/harness-init.sh /tmp/<a-scratch-git-repo>`
- Expected: stdout prints `/tmp/<a-scratch-git-repo>/.claude/scripts/runplan`; that file exists, is executable, and its contents match the template above with `<repoRoot>` substituted; `/tmp/<a-scratch-git-repo>/.git/info/exclude` contains `.claude/scripts/`

- [ ] Implement `bin/harness-init.sh` per the contract above, `chmod +x`
- [ ] Run acceptance check → expected output above
- [ ] Commit: `git add bin/harness-init.sh && git commit -m "feat: add bin/harness-init.sh (per-repo runplan wrapper generator)"`

---

### Task 4: Extend `test/runner-integration.sh` with a `--repo` case

**Wave:** 2
**Blocks:** —
**Blocked by:** Task 1

**Files:**
- Modify: `test/runner-integration.sh`

**Contract:**
- New test case function (follow the file's existing `mkfixture`/`ok`/`bad` pattern already in the file) that:
  1. Builds a plan fixture in a temp dir OTHER than the process's cwd (e.g. via `mktemp -d`, distinct from any dir the test `cd`s into).
  2. Invokes `node <repo>/src/runner.js run --plan <fixture>/docs/plans/<slug>.jsonl --preset <existing-preset-name> --repo <fixture-dir>` from a DIFFERENT cwd (e.g. `$ROOT` or `/tmp`).
  3. Asserts the journal (`<fixture-dir>/runstate/<slug>.jsonl`) and any worktrees are created UNDER `<fixture-dir>`, not under the invoking cwd.

**Acceptance:**
- Run: `bash test/runner-integration.sh`
- Expected: existing cases still `ok`, plus the new `--repo` case reports `ok` (not `FAIL`)

- [ ] Add the fixture + assertions per the contract above, matching existing `ok`/`bad` reporting style
- [ ] Run `bash test/runner-integration.sh` → confirm PASS
- [ ] Commit: `git add test/runner-integration.sh && git commit -m "test: cover runner.js --repo targeting an arbitrary directory"`

---

### Task 5: New `test/harness-release.sh`

**Wave:** 2
**Blocks:** Task 6 (both register a test in `run-tests.sh` — Task 6 must apply its edit after this one lands)
**Blocked by:** Task 2

**Files:**
- Create: `test/harness-release.sh`

**Contract:**
- Same `ok`/`bad`/`PASS`/`FAIL` reporting convention as `test/runner-integration.sh` (`ok() { PASS=$((PASS+1)); ...}`, exit `1` if any `FAIL`, `0` otherwise).
- Cases, each against a scratch `HARNESS_HOME=$(mktemp -d)`:
  1. `bump` with an argument (attempted minor/major) → refused, exit 3, no `versions/` dir created.
  2. `bump` with no argument → succeeds, `versions/<incremented-patch>/src/runner.js` exists.
  3. `release` after that `bump` → `CURRENT` contains the bumped version.
  4. Fixture: after `bump` to version A then `bump` again to version B then `release` (defaults to B) — confirm a hypothetical run "pinned" to A (i.e. a script that reads `versions/A/src/runner.js` directly, bypassing `CURRENT`) still resolves version A's bundle unchanged, proving `bump`/`release` never mutate or delete an earlier `versions/<v>/` dir.

**Acceptance:**
- Run: `bash test/harness-release.sh`
- Expected: all 4 cases `ok`, exit 0

- [ ] Implement `test/harness-release.sh` per the contract above, `chmod +x`
- [ ] Run it standalone → confirm PASS
- [ ] Add it to `run-tests.sh`'s test list (follow the existing pattern for how other `test/*.sh` files are registered there)
- [ ] Commit: `git add test/harness-release.sh run-tests.sh && git commit -m "test: add test/harness-release.sh covering bump/release isolation"`

---

### Task 6: New `test/ensure-web-central.sh`

**Wave:** 3
**Blocks:** —
**Blocked by:** Task 1, Task 5 (file overlap on `run-tests.sh` — see Wave Plan note)

**Files:**
- Create: `test/ensure-web-central.sh`

**Contract:**
- Same `ok`/`bad`/`PASS`/`FAIL` reporting convention as the other `test/*.sh` files.
- Case: copy `src/runner.js` (or the whole `src/`/`bin/ensure-web.sh` bundle, matching whatever subtree `ensureWebUi` needs) into a scratch dir OUTSIDE this repo (e.g. `/tmp/ensure-web-central-XXXX/src/runner.js` + `/tmp/ensure-web-central-XXXX/bin/ensure-web.sh`), then `require()` that copy's `ensureWebUi` (via `node -e`) passing an arbitrary `repoRoot` that does NOT contain a `bin/ensure-web.sh` of its own.
- Assert `ensureWebUi` still finds and (attempts to) run the script that ships next to the copied `runner.js` — i.e. resolves via `shellPath` (`__dirname`-relative), not the passed-in `repoRoot`. A minimal way to assert this without actually starting the web server: stub `bin/ensure-web.sh` in the scratch copy with a script that just echoes a sentinel URL and exits 0, then assert `ensureWebUi(repoRoot)`'s return value equals that sentinel.

**Acceptance:**
- Run: `bash test/ensure-web-central.sh`
- Expected: `ok`, exit 0 — proving `ensureWebUi` resolves via `shellPath` even when `repoRoot` has no `bin/ensure-web.sh` of its own

- [ ] Implement `test/ensure-web-central.sh` per the contract above, `chmod +x`
- [ ] Run it standalone → confirm PASS
- [ ] Add it to `run-tests.sh`'s test list
- [ ] Commit: `git add test/ensure-web-central.sh run-tests.sh && git commit -m "test: add test/ensure-web-central.sh covering shellPath-relative ensureWebUi"`
