A Claude Code plugin that turns a code-change request into a reviewed, merged change, driven by a team of subagents working in git worktrees.
The point is not the agents, it is the enforcement. Every gate is a hook, so it fires
whether or not the model cooperates. A prompt that says "never merge yourself" is a
suggestion; a PreToolUse hook that refuses git merge is not. As this repo's own
dependency rule puts it: a deny is ~100% respected, a CLAUDE.md line is not.
The orchestrator classifies the request, the planner breaks it into tickets, a developer implements each one in its own worktree, the quality-reviewer reviews it, the merger merges it into a session branch, and the whole feature is reviewed once more before promotion. Around that, hooks:
- create and tear down the worktrees, so no agent invents its own layout
- run the validation chain (format, typecheck, lint, unit) on every developer stop, and reject the stop until it is green
- refuse to dispatch a merger without a recorded review verdict
- refuse to promote while approved work sits unmerged
- run the e2e suite once, at end of feature, only after the feature review approved, and reject the orchestrator's stop if it tries to finish while that suite is red
- block the commands that would make the pipeline look healthy while being broken: merging outside the merger, launching arbitrary containers, opening a headed browser
441 tests cover those hooks. That number matters: the guards are the product, and an untested guard fails silently. One of them had been inert for months before a test caught it.
/plugin marketplace add marmelab/AIHarness
/plugin install aiharness
Then declare your project's facts in harness.config.json at the repo root. The minimum:
{
"name": "myapp",
"containers": { "allow": [] },
"validation": {
"steps": [
{
"id": "typecheck",
"kind": "typecheck",
"command": "npm run typecheck"
},
{
"id": "unit",
"kind": "unit",
"runner": "vitest",
"changedScoped": true
}
],
"extraForbidden": ["build", "e2e"]
},
"roles": {
"orchestrator": { "model": "sonnet", "pipeline": false },
"planner": { "model": "opus", "pipeline": true },
"developer": { "model": "sonnet", "pipeline": true },
"quality-reviewer": { "model": "opus", "pipeline": true },
"merger": { "model": "haiku", "pipeline": true }
}
}This repo's own harness.config.json is a working reference, and
node scripts/check-config-sync.mjs tells you whether your roles cover the hook matchers.
The harness is opt-in per request: nothing routes through it until you ask, with
#harness or "use the agent team".
The plugin declares a TypeScript language server (via npx, so nothing to install) and
tells the developer, the reviewer and the planner to resolve symbols through the LSP
tool instead of grepping for them. That matters more than it sounds: a grep goes through
Bash, and in an interactive session Claude Code asks a model to analyse each shell command
before running it, which costs seconds per call. An LSP call does not go through the
shell at all.
Only one LSP server can own a file extension. The runtime registers the first one and
the others never start; the order is undefined and there is no priority field, so a plugin
cannot win, yield, or even detect that it lost. If you also have the official
typescript-lsp plugin enabled, it may claim .ts first — and it ships no binary, so
every call answers Executable not found in $PATH and the agents silently fall back to
grep. Nothing fails; the run just gets slower and more expensive.
Two things report it, both automatic:
- Every session start prints one line naming the conflicting plugin and the remedy.
npm run check(in this repo) fails on the same condition.
To fix it, disable the other plugin — /plugin, then Manage, then toggle it off — or set
it to false in enabledPlugins. Check the user scope (~/.claude/settings.json),
not just the project's: a plugin enabled there is invisible to anything the project does,
which is exactly how this went unnoticed for two full runs. Alternatively, keep the other
plugin and install the binary it expects (npm install -g typescript-language-server typescript).
Verify with any TypeScript symbol: an LSP workspaceSymbol call should return locations
rather than an error.
The split is documented in HARNESS-SPLIT.md, including the
measurements it rests on. In short: this repo owns the hooks, the agent team, the
mechanics rules and the generic skills. You own harness.config.json and your domain
skills. Backends and launcher surfaces are adapters, dormant until your config activates
them.
This repo used to be a copy-paste kit aimed at working WITHOUT a harness. Five of those
files were redundant once the harness arrived and were dropped; HARNESS-SPLIT.md says
which, and what already owns each of them.
One caveat worth knowing before adopting it for a non-Supabase stack: four agent prompts still inline Supabase specifics, so you would have to edit them. Moving those into the adapter is the next planned step.
npm install
npm test
No runtime dependencies: the hooks use only node builtins, and vitest is the single devDependency.
Copy hooks/, agents/, rules/, skills/, commands/ and scripts/ into your
project's .claude/, and merge hooks/hooks.json into your .claude/settings.json
(replacing ${CLAUDE_PLUGIN_ROOT} with $CLAUDE_PROJECT_DIR/.claude). The core resolves
its own files in either layout.
One thing does not carry over: the browser tools. A plugin exposes an MCP server as
mcp__plugin_aiharness_<server>__<tool>, which is the name the developer and the
quality-reviewer declare, so a .mcp.json of your own naming the server playwright
grants them nothing (silently, which is how this went unnoticed for weeks). Name that
server plugin_aiharness_playwright to keep them.