Build, QC, and ship multiple features in parallel in isolated lanes—full app clones with dedicated ports, databases, and Redis instances. Describe a feature in a sentence, and /ship-feature takes it from implementation to delivery while leaving your own checkout untouched.
intake → plan → implement → CI gates + dev preflight → e2e → code review
→ browser QC → senior GO/NO-GO → open PR (base development) + ticket → CI watch
→ watch PR → (human merges) → dev QC → done — or follow-up fix PR and around again
PRs base on and target development; a human merges them. The harness never
pushes development — it validates the feature branch, publishes the PR only
after every gate passes, then verifies the deployed result post-merge and opens
follow-up fix PRs until dev is clean.
The harness is project-agnostic: everything stack-specific lives in a
profile (profiles/<name>/, scaffolded from profiles/_template — profiles
stay local to each install and are never committed). To run it against your own
repo, see Adopting for your own project.
| Tool | Why |
|---|---|
| git, Docker | clone/branch/merge lanes; the one shared Postgres + Redis all lanes use |
| Node ≥ 18 | the dashboard + the Playwright MCP servers (browser QC). The newest ≥18 is auto-selected at runtime, so multiple Node versions coexist fine |
| gh (authenticated) | PRs, CI status, the review loop |
| python3 | bin/ helper scripts (stdlib only) |
| flock | cross-lane locking (macOS: brew install flock; Linux: usually preinstalled) |
| Claude Code | runs /ship-feature + /review-prs. The installer auto-adds the superpowers plugin; code-review is built in |
| your app's toolchain | whatever your repo builds/tests with — invoked through your profile's hooks, never by the harness |
Your app repo must already clone, install, and run locally on its own — the harness builds on top of it.
Everything you set lives in config/:
-
config/local.env— machine-local topology (untracked; create the file). Set at leastPROFILEandSOURCE_REPO; use${VAR:-…}fills so explicit env exports still win:PROFILE="${PROFILE:-myapp}" # uses profiles/myapp/ SOURCE_REPO="${SOURCE_REPO:-$HOME/work/myapp}" # the app repo lanes clone from LANES_ROOT="${LANES_ROOT:-$HOME/work}" # where laneN/ clones are created DB_PREFIX="${DB_PREFIX:-myapp_l}" # lane N DB = myapp_l<N>
Shared defaults (ports, Postgres/Redis endpoints,
COMPOSE_FILE) live in the trackedconfig/lanes.env; override them here only if they clash. -
config/secrets.env— credentials (gitignored). Copy the example and fill it in:cp config/secrets.env.example config/secrets.env
Key For SEED_USER_EMAIL/SEED_USER_PASSWORDthe local seed account each lane DB gets (also the qc-local login) DEV_QC_PASSWORDper-lane dev-QC accounts (dev-site QC integration) TRACKER_EMAIL/TRACKER_PASSWORDtracker login (ticketer integration) QC_UPLOAD_FIXTURES_DIRabsolute path to upload fixtures; blank = skip upload scenarios Your app's own LLM/API keys stay in your repo's
backend/.env— each lane's.envis seeded from it withDATABASE_URL/REDIS_URL/UPLOAD_DIRrewritten per lane, so isolation holds.
Run from the harness repo root:
bin/db-shared-up.sh # shared Postgres + Redis (one set for all lanes)
bin/install-claude-assets.sh # copy skills + agents into ~/.claude (+ superpowers plugin)
bin/dashboard.sh start # → http://127.0.0.1:8090
install-claude-assets.shcopies the skills/agents (it bakes in install-time paths), so re-run it after editing any source skill/agent and restart open Claude sessions — skills, agents, and plugins load at session start.
Click + Add lane in the dashboard top bar — it provisions the next free lane end to end (clone → deps → DB → MCP config → agents). Or from a terminal:
bin/lane-bootstrap.sh 1 # run from a terminal so it can prompt for the dev-QC accountLanes 1–9 are supported; 3–5 live at once is typical. Each runs its own API
(:800N) and frontend (:300N).
cd ../lane1 && claude
# /ship-feature "Add a CSV export button to the reports table"Answer the opening questions once, then watch the dashboard. The lane runs unattended and only stops on a genuine blocker (an escalated senior NO-GO, an ambiguous merge conflict) — the reason shows in the card's notes.
Open a separate terminal/session per lane — they're fully isolated and run in parallel:
# Terminal 1 # Terminal 2
cd ../lane1 && claude cd ../lane2 && claude
# /ship-feature "Feature A" # /ship-feature "Feature B"Prefer a front door? bin/lane-assign.sh "Feature A" reserves + boots the next
free lane and prints the exact cd … && claude + /ship-feature to run.
Dedicate a lane to reviewing PRs:
cd ../lane4 && claude
# /review-prsAfter a short scope Q&A it polls open PRs and posts substantive feedback on any that are new, have new commits, or have new discussion aimed at it — never merging, pushing, or closing. A per-lane cursor means it never re-reviews unchanged PRs. The dashboard shows a review-history table for that lane instead of the pipeline map.
When a feature is done: click 🧹 clear (archive its state + reset the status
fields), then ⟳ reset if the next feature needs a clean branch + fresh DB,
then assign the new one (cd ../laneN && claude → /ship-feature "…"). Cleared
features stay browsable in the proof gallery's feature picker.
Each lane's live app is at http://localhost:300N (log in with your seed
account) — open several at once. Lane lifecycle is on the dashboard card buttons
(see Features) or the CLI. Lanes serve production
builds (no hot reload), so after hand-editing a lane run bin/lane-up.sh N to
rebuild.
Resource note: heavy steps (FE build, e2e, the integration push) are serialized across lanes; dev-site QC runs in parallel. Idle lanes are light (~340 MB each) — run fewer if memory is tight. Don't
docker compose stopthe shared DB while lanes are up.
Click any lane card to see its pipeline map — every stage, color-coded:
| Color | Meaning |
|---|---|
| Green | Done — stage completed successfully |
| Blue | Current — running now |
| Amber | Passed without evidence — worth verifying it ran |
| Gray | Pending — not reached yet |
| Red dashed | Fail path — leads to fix-on-branch, then re-entry |
Each card shows, at a glance: feature title and current stage with a progress bar, a ⏱ time-on-phase timer, stack health (api/fe up/down), the gate decision (GO / NO-GO / pending), git branch + latest commit, CI status, PR / ticket / test-URL links, a heartbeat with per-lane liveness (DEAD — loud red — when a running/watching lane stops reporting; calm idle when an old heartbeat is expected), and notes.
All lane management is on the card — no CLI needed for day-to-day use:
| Button | What it does |
|---|---|
| ▶ up | Boot/rebuild the lane stack (api + worker + frontend) |
| ■ down | Stop the lane (kills processes, frees ports); code + DB preserved |
| 🧹 clear | Archive the feature's state and reset status fields — the "done with this feature" tidy-up |
| ♻ agents | Re-push current harness templates into the lane (agents + MCP config). Use after editing harness files |
| 🔑 creds | Set/change the lane's dev-QC account, then re-embed it in the agents + re-seed sessions |
| ⟳ reset | Clean integration branch + fresh DB seed (confirms first) |
| 🗑 remove | Delete the lane entirely — clone + DBs + state (confirms first) |
| + add lane | (top bar) provision the next free lane |
After ♻ agents or 🔑 creds, restart that lane's Claude session — agents and MCP servers load at session start.
The selected lane's panel shows thumbnails of every screenshot the QC agents captured, grouped per feature and per phase:
- qc-local — the local browser-QC agent (against the lane's feature-branch stack)
- qc-dev — the background dev-site QC agent (against the deployed dev site)
- Click a thumbnail for a full-size lightbox; click +N for the full gallery
- A 🎫 task report link appears once the ticketer agent has written one
- The feature picker switches the whole detail panel to a past feature's archived state (final stage, gate, PR/ticket links) while the lane keeps running
When a lane is running /review-prs, clicking its card shows a review-history
table (PR # · action · when · summary) instead of the pipeline map.
Want the visuals?
docs/screenshots/regenerate.cjscaptures the dashboard views from YOUR running install (they're kept out of git).
Everything stack-specific lives in one profile at profiles/<name>/. bin/,
dashboard/, and the skills/agents stay generic; profiles are untracked, so
your project's specifics never enter this repo. A profile has five parts:
| File | What |
|---|---|
profile.env |
stack shape — DB driver, API path, service dirs, branch names, compose service names |
hooks/ |
7 small scripts the engine calls for stack-specific steps (below) |
integrations.env |
optional tracker / dev-site-QC / CI-deploy-wait toggles (off ⇒ that stage is skipped) |
review-checks.md |
stack-specific senior-gate / PR-review checks (empty ⇒ generic review) |
tracker-notes.md |
provider login + idempotency notes for the ticketer (empty ⇒ plain email+password login) |
The fast path — in a Claude Code session, run /harness-adapt. It
inspects your repo (package managers, framework, build/test/migrate commands,
compose services, branches), drafts profiles/<name>/ from _template, asks you
to confirm what it can't infer, validates with harness-doctor, and dry-runs a
lane. By hand: cp -R profiles/_template profiles/myapp, fill profile.env, and
replace each hook's TODO stub (each stub documents a worked example inline).
Then set PROFILE + SOURCE_REPO in config/local.env and validate until clean:
bin/harness-doctor.sh myapp # fix every ✗ (stub hooks, missing config) → 0 errorsBefore calling any hook, the engine exports a stable env contract — use these in
your hooks: LANE / LANE_DIR, API_PORT / FE_PORT, DATABASE_URL /
REDIS_URL / UPLOAD_DIR, DB_NAME, API_BASE / FE_URL, RUN_DIR /
LOG_DIR, PROFILE_DIR / SOURCE_REPO / HARNESS_ROOT, plus everything in your
profile.env. Two helpers are available: harness_spawn <name> <workdir> <cmd…>
(background a service with detached stdio + pid/log tracking — use in boot) and
die <msg>.
| Hook | Must do | Notes |
|---|---|---|
bootstrap |
install deps in the lane clone | — |
migrate |
apply DB migrations to DATABASE_URL |
— |
seed |
create the initial user/org | SEED_USER_* exported |
boot |
build + start services, then return | use harness_spawn; $1 == --no-build ⇒ reuse build |
health |
exit 0 only when fully ready | prefer curl --retry |
ci-gate |
lint / test / contract; non-zero on failure | gets an isolated TEST_DATABASE_URL |
e2e |
run e2e against the running stack, under the e2e lock | with-lock.sh e2e -- <cmd> |
DB create/drop and Redis-index assignment stay harness-side (driven by your
PG_* / COMPOSE_SERVICES / DB_PREFIX) — the harness assumes Postgres +
Redis via your repo's docker-compose.
Each is off by default; turn on what you have in integrations.env:
- Tracker (
TRACKER_ENABLED=1) — file a ticket when the PR opens. SetTRACKER_PROVIDER,TRACKER_URL,TRACKER_PROJECT,TRACKER_STATUS,TRACKER_ASSIGNEE,TRACKER_MCP. Write your provider's login + issue-search notes intracker-notes.md(a plain email+password form needs none). - Dev-site QC (
DEV_QC_ENABLED=1) — browser-QC the deployed dev site after a human merges the PR. SetDEV_SITE_URL,DEV_QC_MCP. Each lane uses its own dev account (passwordDEV_QC_PASSWORD); the account must exist on your dev server. - CI deploy-wait (
CI_WAIT_ENABLED=1) — wait on a deploy before dev-QC. SetCI_REPO+CI_DEPLOY_CONTEXT(a GitHub commit-status context; works for CircleCI, GH-Actions, etc.).
The QC/tracker agents drive Playwright MCP servers by the names you set;
register those servers in your source project's MCP config (harness-doctor
reminds you which names each enabled integration expects).
| Symptom | Fix |
|---|---|
| Lane shows DEAD | a running/watching lane has no heartbeat for >10 min — its Claude likely died; cd ../laneN && claude to resume (tune with DEAD_SEC=… bin/dashboard.sh restart; STALL_SEC still honored as a legacy alias) |
| Lane session missing an MCP | bin/lane-mcp-sync.sh N, then restart the lane's Claude session (MCPs load at start) |
| Agent can't post a PR comment | per-lane allow-rules load at session start — restart the lane's Claude session after first provisioning |
| Server won't boot | check logs/laneN/{api,fe,worker}.log |
flock: command not found |
brew install flock (macOS) |
harness-doctor shows stub hooks |
implement profiles/<name>/hooks/*.sh |
Most operations are on the dashboard; these are for scripting or terminal use.
# Setup
bin/db-shared-up.sh # shared Postgres + Redis
bin/install-claude-assets.sh # skills + agents → ~/.claude (+ superpowers)
bin/dashboard.sh start|stop|restart # watch UI → :8090
bin/harness-doctor.sh [profile] # validate a profile's config + hooks
# Lanes (also on the dashboard)
bin/lane-bootstrap.sh N # provision lane N
bin/lane-add.sh [--and-up] # provision the next free lane
bin/lane-assign.sh "feature" # pick + boot a free lane, print the run command
bin/lane-up.sh N [--qc] | bin/lane-down.sh N # boot/rebuild (--qc = profile QC env) | stop a lane
bin/lane-reset.sh N | bin/lane-remove.sh N # clean dev + reseed | delete entirely
bin/lane-status.sh # status table for all lanes
# Maintenance
bin/lane-mcp-sync.sh N|--all # re-sync MCP config to lane(s)
bin/lane-agents-install.sh N|--all # regenerate per-lane agents with current creds
bin/lane-qa-login.sh N [dev|local|all] # re-seed a lane's browser login session<parent>/ # e.g. ~/work — set by LANES_ROOT / SOURCE_REPO in config/lanes.env
├─ <your-app-repo>/ # your app (you provide; SOURCE_REPO points here)
├─ harness/ # this repo — engine, dashboard, skills, agents
│ ├─ bin/ # lifecycle scripts (generic)
│ ├─ dashboard/ # watch UI — Node + React (generic)
│ ├─ claude/ # skill + agent templates (generic)
│ ├─ profiles/ # per-project config: _template/ + your <name>/ (untracked)
│ ├─ config/ # lanes.env + local.env/secrets.env (both gitignored)
│ └─ docs/screenshots/ # regenerate.cjs — optional local dashboard screenshots
└─ lane1/ lane2/ … # lane clones (the harness creates these)
- You edit:
profiles/<name>/,config/local.env,config/secrets.env. That's it for a normal adoption. - Generic (don't touch):
bin/,dashboard/, the skills, and the agent templates — they read your injected config. - Never committed (gitignored):
config/local.env,config/secrets.env,profiles/<name>/(only_templateships),docs/reports/+ screenshots,state/,run/,logs/,locks/, and per-lane credential files inside each lane clone (<lane>/.harness-qa.env,<lane>/.claude/agents/*.md). Passwords and project specifics never enter git.