agent-duet
# agent-duet
**Have Claude write the code, have Codex review it, then have Claude answer the review —
automatically, in one command, with nothing committed until you say so.**
You type `/duet <what you want done>`. Three separate AI sessions run one after another,
your tests run, and then it stops and shows you what happened. You decide whether it ships.
---
## Why bother
Asking one model to review its own work does not really work — it agrees with itself. The
usual fix is to copy the diff into a second tool by hand, paste the critique back, and
keep track of which round you are on. That is the whole job this does for you.
Three rules make it worth the trouble, and none of them is a promise in a prompt — each
one is enforced by code:
- **The implementer never reviews its own work.** The reviewer is a different CLI, in a
different process, with no memory of writing the code.
- **The reviewer cannot edit anything.** agent-duet fingerprints the repository before and
after the review and compares. If the reviewer touched a file, you are told.
- **Neither one can publish.** Committing and pushing live in a separate tool call that
only runs after you approve.
---
## How a run works
```
you ──▶ /duet "add retry-with-backoff to the HTTP client"
│
┌─────────────▼─────────────┐
│ 1. Claude Code implements │ a fresh process, no history
└─────────────┬─────────────┘
┌─────────────▼─────────────┐
│ 2. Codex reviews │ read-only, verified read-only
└─────────────┬─────────────┘
┌─────────────▼─────────────┐
│ 3. Claude Code reconciles │ fixes what is justified, argues what is not
└─────────────┬─────────────┘
┌─────────────▼─────────────┐
│ 4. your tests run │
└─────────────┬─────────────┘
▼
it STOPS and reports ──▶ you approve ──▶ commit (+ optional push)
```
A focused change often takes **5 to 20 minutes**. A whole-project audit can take hours.
Three real AI sessions run end to end, and the work keeps going if you close your
terminal.
---
## Install
```bash
git clone https://github.com/slyfox1186/Claude_Code_Codex_MCP_Coordinator.git
cd Claude_Code_Codex_MCP_Coordinator
./setup.sh -d /path/to/your/project
```
Use `--directory` instead of `-d` if you prefer the long form. You can also omit the
option and enter the project path when prompted. Setup explains what it needs and asks
for consent before creating an environment or installing a missing provider CLI.
- If Conda is detected, it creates a dedicated environment named `agent-duet`.
Setup never installs Conda or changes `base` or another environment.
- Without Conda, it uses the default Python 3.13+ only to create a private environment;
packages are not installed into system Python.
- If Claude Code or Codex is missing, it offers the official installer. It also offers
sign-in and a throwaway demo. Before consent, setup shows the expected user-local files;
the vendor installers manage their own updates, and Codex may add a `PATH` block to your
shell profile.
- `-d`/`--directory` skips the demo and project-path questions. The value may be relative,
absolute, `~/...`, and may end in `/`.
- The selected project may be an ordinary folder. Setup automatically creates the local
Git baseline Agent Duet needs to compare Claude's and Codex's work. It records every
existing non-ignored file in one local commit, adds no remote, and uploads nothing.
Review the project's `.gitignore` first if it may contain sensitive files.
Requirements: Linux and Git, plus Python 3.13+ when Conda is absent. `curl` or `wget` is
needed only if a provider CLI must be downloaded. See **[INSTALL.md](INSTALL.md)** and read
**[SECURITY.md](SECURITY.md)** before use.
Setup never uses `sudo`. It validates generated configuration, backs up files it replaces
to `<name>.duet-backup`, and is safe to rerun.
Setup merges only `mcp__agent_duet__duet_status` and
`mcp__agent_duet__duet_wait` into Claude Code's user allow rules. Those two tools only
read run state. `duet_start`, `duet_cancel`, and `duet_finalize` keep Claude Code's normal
permission checks, so installation does not preapprove starting, stopping, committing, or
pushing work.
After setup or an upgrade, close and reopen any Claude Code or Codex sessions that were
already running. An open client keeps the MCP process and `/duet` instructions it loaded
at startup; continuing in it can show stale or unverifiable progress.
---
## Use it
Point it at a project during installation or later:
```bash
./setup.sh -d ~/code/my-project
./setup.sh --directory ~/code/my-project
./setup.sh add-repo ~/code/my-project
```
Then work normally, in either CLI:
```
/duet Add retry-with-backoff to the HTTP client and cover it with tests
```
Type `/duet` with nothing after it and it will work out the task from your conversation,
or ask you if there is nothing to work from. Either way it confirms the acceptance
criteria before spending your time.
When it finishes it prints what it did and waits. Say **"finalize"** and it commits the
validated change. If the project has a remote, it can push after approval; without one,
it completes with a local commit. Say nothing and nothing happens.
### Other setup commands
```bash
./setup.sh check # is everything working?
./setup.sh -d ~/code/project # guided setup + this project
./setup.sh --directory ~/code/project # identical long form
./setup.sh add-repo ~/code/project # let it work on a project
./setup.sh remove-repo ~/code/project
./setup.sh demo # a throwaway project to try it on
./setup.sh demo --clean
./setup.sh uninstall
```
If a registered project was moved or deleted, `check` reports that path as a warning
instead of claiming Agent Duet is broken. Restore the folder, or remove the stale entry
with `./setup.sh remove-repo /old/project/path`.
Project registration allows the project's parent directory, detects its test suite
(pytest, `npm test`, `cargo test`, `go test`), and writes the config entry between
markers so `remove-repo` takes it back out cleanly. If the folder has no Git history,
setup automatically creates the same local-only baseline used by `-d`.
For Python tests, setup asks before creating a separate validation environment for that
project. It uses `constraints.txt` to constrain resolution; installs every dependency in
`requirements.txt`, `app/requirements.txt`, `requirements-dev.txt`, and the
test-requirements variants; installs a PEP 621 `pyproject.toml` project when present; then
installs and verifies pytest. Target-project packages never enter Agent Duet's own
environment, Conda `base`, or another project's environment.
Two different projects can run concurrently by default. A project still gets only one
active run, preventing two agent pairs from editing the same checkout. Adjust
`max_parallel_global` from 1 through 16 in `~/.config/agent-duet/config.toml` if your
machine or provider limits call for a different total. Existing explicit settings are
preserved during upgrades.
---
## Where the work ends up
By default a run works **on the branch you are already on**, so finalizing commits there.
If you are on `main`, it commits to `main`. That is what most people mean by "make this
change", and it needs a clean working tree, because the run edits your checkout in place.
If you explicitly want the work isolated, ask for a review branch:
```
/duet <task> — put it on a review branch, I want to look first
```
That runs in a private worktree instead, leaving your checkout completely untouched, and
lands the work on its own `agent-duet/<id>` branch for you to merge. Agent Duet never
selects or suggests this mode merely because your working tree is dirty.
Every interactive `/duet` run passes its mode explicitly. No machine-wide default can
silently create a branch.
---
## Checking its work yourself
Everything a run did is kept on disk. You never have to trust the summary.
```bash
agent-duet runs # every run, newest first
agent-duet logs <run-id> # everything about one run
```
`logs` prints the whole story: every command it ran, every argument, every phase
transition with a timestamp, and why it stopped. It never prints passwords or tokens, so
you can paste it to someone as-is.
Worth confirming once, the first time:
- **The reviewer really was read-only.** Look for `codex_readonly_verified: true`. That is
measured — a fingerprint before and after — not claimed.
- **The run outlives your session.** Close the CLI completely, open a new one anywhere,
and ask for `duet_status` on the run id. Same run, still going.
---
## The tools
`/duet` calls these for you. You can also just ask either CLI in plain words.
| Tool | What it does | Publishes? |
|---|---|---|
| `duet_start` | Validates the repo, creates the run, spawns a detached worker, returns a `run_id` in seconds | No |
| `duet_status` | Durable phase, verified worker/child liveness, timestamps, evidence, next action | No |
| `duet_wait` | The same, after one foreground-safe wait of at most 90 s | No |
| `duet_cancel` | Sets the cancel flag and reaps the worker's process group | No |
| `duet_finalize` | Commits, pushes, verifies the remote ref, runs a deployment verifier | **Yes** |
`duet_start` **cannot** commit, push, deploy, change remotes, or rewrite history — not
because a prompt forbids it, but because that code does not exist on that path.
Publishing lives only in `duet_finalize`, which re-verifies the branch, the remote URL,
and the exact diff fingerprint that was validated before it will touch anything.
---
## Phases
User-facing updates identify the three model phases plainly: **Phase 1 of 3 — Claude
implements**, **Phase 2 of 3 — Codex reviews**, and **Phase 3 of 3 — Claude reconciles**.
Phase 1 is not the final step. The names below are the internal state-machine values.
```
QUEUED -> CLAUDE_IMPLEMENTING -> HANDOFF_VALIDATING -> CODEX_REVIEWING
-> REVIEW_INTEGRITY_CHECK -> CLAUDE_RECONCILING -> FINAL_VALIDATING
-> AWAITING_FINALIZE -> FINALIZING -> COMPLETE
```
If the first authoritative validation fails, one bounded repair path runs:
```
FINAL_VALIDATING -> CLAUDE_VALIDATION_REPAIRING -> FINAL_VALIDATING
```
Claude receives the measured exit code and redacted output, repairs the root cause, and
the coordinator reruns the complete configured validation set. A second failure is
terminal; both attempts remain in the evidence. A failed or cancelled run can never be
published with raw Git through `/duet`—only `duet_finalize` may publish, and only from
`AWAITING_FINALIZE`.
`FAILED`, `CANCELLED`, and `COMPLETE` are terminal. Every transition is written to SQLite
with a timestamp and a reason *before* the work that follows it happens.
Every status also reports server-measured `liveness`: whether the detached worker and
the expected Claude or Codex child process are alive at that instant. `/duet` may call a
model phase active only when that status belongs to the retained `run_id` and reports
`MODEL_ACTIVE`; a lost or unknown result is never treated as progress.
If cleanup could not terminate a recorded worker or child process, status reports
`CLEANUP_REQUIRED`; repeating `duet_cancel` retries cleanup instead of forgetting it.
The run survives the client that started it — the worker is detached, so you can close the
terminal, reopen either CLI, and pick up where you left off.
Status polling does not limit model work. The shipped configuration gives each Claude or
Codex phase a 7,200-second safety ceiling, runs Claude at `xhigh` effort and Codex at
`high` reasoning effort, and applies no Claude dollar cap. Change those quality/runtime
settings in `config.toml` if a particular machine needs a different trade-off.
If the *worker itself* dies, that is terminal. The next `duet_start` on that repository
records the run as `FAILED` with its evidence preserved. There is no phase-aware resume,
and a half-finished run is never silently continued.
---
## When something goes wrong
```bash
agent-duet logs # the most recent run, in full
```
| What you saw | What to do |
|---|---|
| Fails immediately at `CLAUDE_IMPLEMENTING` | run `claude` on its own once and sign in |
| Fails at `CODEX_REVIEWING` | run `codex` on its own once and sign in |
| `not below an allowed_repo_roots entry` | `./setup.sh add-repo <the project>` |
| `already active ... max_parallel_global is N` | all global slots are occupied; wait, finalize, cancel, or raise the configured limit |
| `refusing an in-place run ... dirty working tree` | commit or stash, then retry on the same branch |
| `Denied by auto mode classifier` while polling | from this checkout, run `./setup.sh install`, or add `mcp__agent_duet__duet_status` and `mcp__agent_duet__duet_wait` through Claude Code's `/permissions`; then retry the poll |
| Validation fails twice | run `agent-duet logs <run-id>`; both command results and output tails are preserved |
| Refuses to finalize | read the reason; something changed after the tests ran |
---
## Operator commands
```bash
agent-duet doctor # health report
agent-duet runs # every run, newest first
agent-duet logs [run-id] # everything about one run (default: the most recent)
agent-duet cancel <run-id> # clear an unfinished run and free its slot
agent-duet gc --older-than 30 # dry run: list what old terminal runs left behind
agent-duet gc --older-than 30 --apply
```
`cancel` accepts an id prefix. It exists because a run parked at `AWAITING_FINALIZE` has
no live worker and is never reaped — it is waiting for a person — yet it still counts as
active, so with the default `max_parallel_global = 2` it occupies one of the two global
slots until someone finalizes or cancels it.
`gc` forgets a terminal run completely: its artifact directories, git's worktree
registration in the real repository, and its row in the listing. It never deletes a run's
branch — that holds the work — so it reports the branches it orphans instead. It only ever
touches directories inside the state tree, and only for terminal runs.
---
## Where things live
```
~/.config/agent-duet/config.toml 0600
~/.local/state/agent-duet/state.sqlite3 0600, WAL
~/.local/state/agent-duet/runs/<run-id>/ 0700 prompts, logs, artifacts, manifests
~/.local/state/agent-duet/worktrees/... 0700 private worktrees (review-branch runs)
~/.local/state/agent-duet/locks/ 0700 one flock per repository
```
Keep the state directory off NFS, Dropbox, and Syncthing. Two machines must never share
it; they coordinate through git remotes, not through this database.
The config file must be a regular file you own, and must not be group- or world-writable:
it names the executables to run and the exact command vectors the coordinator will
execute, so write access to it is equivalent to code execution. Loading refuses otherwise.
---
## Installing by hand
Everything `setup.sh` does is ordinary configuration; nothing is hidden. Copy
`config.example.toml` to `~/.config/agent-duet/config.toml`, replace every `REPLACE_ME`,
`chmod 700` the config and state directories and `600` the config file, then run
`agent-duet doctor`. Register the server:
```bash
claude mcp add-json --scope user agent_duet \
'{"type":"stdio","command":"<path>","args":[],"env":{},"timeout":120000}'
codex mcp add agent_duet -- <path>
```
Merge these two exact read-only rules into `permissions.allow` in Claude Code's user
settings; do not replace unrelated settings and do not allow `mcp__agent_duet__*`:
```json
{
"permissions": {
"allow": [
"mcp__agent_duet__duet_status",
"mcp__agent_duet__duet_wait"
]
}
}
```
Use `/permissions` when changing the rules interactively. Then add
`tool_timeout_sec = 120` and `enabled_tools` to the `[mcp_servers.agent_duet]` table Codex
wrote. `duet_wait` returns within 90 seconds so it stays below Claude Code's two-minute
MCP auto-background threshold. Copy `commands/duet.md` into `~/.claude/commands/` and
`~/.codex/prompts/`. If an older manual installation reports
`Denied by auto mode classifier`, run `./setup.sh install` or add the same two rules
through `/permissions`.
---
## Security posture
Child agents run with **full, unsandboxed machine access** by deliberate operator
decision. Read **[SECURITY.md](SECURITY.md)** before installing this anywhere you would
not hand the same access to a person.
---
## More
- **[INSTALL.md](INSTALL.md)** — the short server installation guide
- **[HOW_TO_TEST.md](HOW_TO_TEST.md)** — try it in three commands
- **[SECURITY.md](SECURITY.md)** — the trust model and every guard, in detail
- **[HOW_TO_BUILD_THIS.md](HOW_TO_BUILD_THIS.md)** — installing it on another machine,
step by step with a check after every step. Written to hand straight to Claude Code on
the target PC.
TDQS
Scored across 5 tools
Each tool maps to a distinct lifecycle stage (start, status, wait, cancel, finalize). The only mild overlap is duet_wait vs duet_status, since both return status, but the descriptions clearly distinguish waiting-for-phase-change from a plain status read.
All five tools use the same duet_ prefix followed by a concise verb (start, status, wait, cancel, finalize) in consistent snake_case. The pattern is predictable and readable.
Five tools cleanly cover an orchestration lifecycle without redundancy. Each tool earns its place and nothing feels padded or missing for the stated scope.
The surface covers the full run lifecycle: start, poll status, wait for phase change, cancel, and finalize/publish. Nothing essential (create, monitor, abort, commit) is missing for a single-run orchestration model.