Two AI models build it.
The harness checks the rules.
A single agent grades its own homework and reports that it passed. duet puts a second model in the loop, runs your tests itself, and holds each workflow to a rule it checks against what actually happened — not what either model says happened.
curl -fsSL https://raw.githubusercontent.com/shubharya-os/duet/main/install.sh | sh
Or inside Claude Code, as a plugin:
/plugin marketplace add shubharya-os/duet then /plugin install duet@duet.
Uses the Claude and ChatGPT plans you already have — no API keys. One subscription works too. GitHub →
Nine commands, each with a checked rule
Each one tells the pair its rule — and then checks it.
duet build "…"
Start from nothing. The gate is red before any code exists, and a gate already green on an empty directory is refused.
duet fix "…"
The tests are replayed against a snapshot of the original code. They must fail there and pass now.
duet add "…"
A test must fail against the code as it was before. A whitespace edit to a test file does not count.
duet refactor "…"
Every test that existed at the start must end byte-for-byte unchanged. Refused if the suite starts red.
duet plan "…"
Both argue it out into PLAN.md. Any other file changing clears both sign-offs.
duet review
A second model reads your diff, held read-only by its backend — not asked politely.
duet make "…" --like URL
Research the references from public sources, build, then score parity in the harness. It will not finish while the reference's name, domain or colours are still in the product.
duet jury
Simulated users, a paying client, an investor, judges and a skeptic. Every objection must quote the product, and the harness checks the quote is really there.
duet page check
A website's gate: renders the page in Chrome at desktop and phone widths and fails on what is wrong on screen.
An app from one sentence, then attacked
An expense-tracker web app — single-page UI, JSON API, SQLite, CSV export — from an empty directory.
Then attacked by hand — over HTTP, and in a real browser — not through its own tests:
| attack | result | detail |
|---|---|---|
| SQL injection in a category | stored as text | x'); DROP TABLE expenses;-- — table intact |
| stored XSS, loaded in a browser | rendered as text | 0 injected elements; no external resources loaded |
| CSV formula injection | defused | =cmd|… exported as '=cmd|… — added unprompted |
0.1 + 0.2 | exactly 0.30 | money kept as integer cents |
| 30 simultaneous writes, then restart | all persisted | 36 of 36 rows |
| 10 kinds of malformed request | right code each | 400, 404, 411, 413, 415 |
| bind address | 127.0.0.1 | stated in the spec as the first default |
Why the tests are replayed
The first version of fix asked only whether the gate had ever gone red. The first live run beat it.
claude-sonnet-1 DONE state 8e75764a test and fix written in one turn claude-sonnet-2 DONE state 8e75764a ✗ both signed off, but the `fix` rule is not met claude-sonnet-1 CONTINUE state 0b73182d, gate FAILING re-broke working code on purpose ... BOTH AGENTS SIGNED OFF on 8e75764a byte-identical to the rejected state
The rule was met by ceremony. So now the harness copies a snapshot of the original code, lays the finished tests over it, and runs them there.
| history rule | replay | |
|---|---|---|
| rounds | 6 | 3 |
| vetoes of a correct fix | 1 | 0 |
| turns spent re-breaking code | 1 | 0 |
It becomes the default
duet skill default — then the same bug report, word for word, in a fresh Claude Code session.
| before | after | |
|---|---|---|
| used duet | no | started duet fix on its own |
| tests for the bug | none added | 8 added; 6 fail on the original with the reported error |
| time | ~1 min | 6 min |
Questions, exploration and one-line edits stay direct. Always-on cost: about 545 tokens of instructions per session.
Coming from ECC?
Both install with one command. The difference is what you carry afterwards, and what gets checked.
| ECC 2.2.2, full install | duet | |
|---|---|---|
| to learn | 386 skills, 68 agents, hooks, profiles | 6 commands |
| every session | ~41,500 tokens (smaller profiles exist) | ~545 tokens |
| "test first" | an instruction | checked by replay |
| reviewer | same model, another role | a second model |
| domain skills | ~300 included | use any you install |
duet from-ecc # what you actually used, from your own logs duet from-ecc --keep <name> # keep it, licence intact claude plugin uninstall ecc@ecc # you run this duet skill default
On the machine it was built on: two ECC agents in use, once each, out of 454 skills and agents.
Run against ECC
Same brief, same model, ECC installed with its own agents. One build, two plans, graded blind.
| duet | ECC | |
|---|---|---|
| build an app from one spec | finished, 35 min | unfinished at 82 min |
| security & validation attacks | all held | all held |
| 100 simultaneous writes, x3 | 300/300 | 218/300 |
| planning pitfalls caught (blind) | 4/5 · 5/5 | 4/5 · 5/5 |
| planning time, full debate (before v0.11) | 8–10 min | 3–4 min |
| planning, full debate, v0.11, side by side | 5/5, 3 min 43 s | 5/5, 2 min 56 s |
planning, --quick, side by side | 4/5, 5 min 59 s | 4/5, 5 min 23 s |
ECC won planning speed; the rest is in docs/QA.md, caveats included.
Where it runs
| harness | /duet | default for real changes |
|---|---|---|
| Claude Code | ✓ | ~/.claude/CLAUDE.md |
| Codex | $duet | ~/.codex/AGENTS.md |
| Gemini CLI · Google Antigravity | ✓ | ~/.gemini/GEMINI.md (shared) |
| OpenCode | ✓ | its AGENTS.md, or your CLAUDE.md |
| Cursor · any AGENTS.md reader | — | duet skill default --project |
What it cannot do
Two models can agree and still be wrong — one session ended in consensus on a CLI that
crashed on Python 3.9, because the gate ran on 3.12 and the criteria never named a version. The gate
only proves what it runs. fix cannot tell that a failure on the original code is this
bug, and says so when the only failure is an import. Every one of these is written down, with the
session that found it, in docs/QA.md.