magi
A blind multi-agent implementation competition, as a CLI.
Three agents solve the same task in isolation. Three judges rank the results without knowing who wrote what. If they disagree, they argue, and then vote privately. The winner survives a review + real-machine verification loop before anything is allowed to merge.
The premise is that one agent's first choice of approach is the part no amount of reviewing fixes. Review a bad design carefully and you get a well-polished bad design. So magi does not review one implementation — it holds an election between several, and only then reviews.
flowchart LR
prep[prep: worktrees + blind hook] --> impl[implement x N]
impl --> judge[judge x M, blind]
judge -->|first choices agree| vote[final vote, private]
judge -->|split| delib[deliberate]
delib --> vote
vote --> tally[mechanical tally]
tally --> fold[fold losers]
fold --> review[review x R + E2E + fix, bounded]
review --> gate[gate commands]
gate --> merge[merge]
What makes the judging blind
A judge that knows which model wrote a candidate stops grading the patch and starts voting on the model's reputation. Four mechanisms prevent that:
| Mechanism | Where |
|---|---|
Candidates are A/B/C by seeded shuffle; each judge gets its own presentation order |
src/blind.rs |
Branches are named after the label (magi/<run>/B), so a judge can git show a candidate without learning its author |
src/run.rs |
A per-worktree commit-msg hook deletes Co-Authored-By: / Generated with … before they can land |
src/blind.rs, src/git.rs |
| The same trailers are stripped again at presentation time, and uncommitted work is rescued under a neutral identity | src/blind.rs, src/git.rs |
The facilitator is code, not an agent. magi assigns the labels, relays the transcript, and collects the final votes one-to-one. A moderator that never learns an author cannot leak one, and there is no seat for a model to be persuaded out of neutrality.
Two more consequences of that design:
- Seats, not agents. Conversations are keyed by seat (
impl-B,judge-2), never by agent id. The model that wrote candidate B and the model sitting as judge 3 may be the same model, and it still cannot recognise its own work, because the judge seat's conversation never contained the implementation. - Private final votes. After deliberation, each judge is asked separately, told that nobody sees the answer, and no running tally exists to drift toward.
Vendor names appearing in the patch body are a different problem: redacting
them would corrupt the artifact under judgement. magi scans for them, records
what it found, and blind.on_leak decides (warn by default, or redact /
fail).
Conversation continuity
Deliberation and the review loop are multi-turn. Re-sending three patches on every turn is both expensive and worse — a judge should argue from what it already said. magi therefore keeps one CLI conversation per seat:
| CLI | open | resume | notes |
|---|---|---|---|
claude |
--session-id <uuid> (magi mints it) |
--resume <uuid> |
addressable before the first turn |
opencode |
run --format json reports sessionID |
run -s <id> |
id captured from the event stream |
agy (Antigravity) |
--output-format json reports conversation_id |
--conversation <id> |
--print-timeout is raised to the node budget |
codex |
exec --json reports thread.started.thread_id |
exec … resume <id> |
resume is a subcommand: every option precedes it. The prompt goes on stdin |
When a seat has no live conversation — sessions disabled, or a first turn that
never reported an id — magi re-sends the full context instead of letting the
agent argue from memory it does not have. Set graph.sessions = false to force
that everywhere.
Gemini CLI is deliberately not supported: Google retired the standalone
client for individual accounts in favour of Antigravity, so the adapter would be
dead code. kind = "command" covers anything else.
Install
magi drives subscription CLIs, not API keys: claude, opencode, agy,
codex, or
any command you point it at. It spends your existing plan and nothing else.
Use
magi run starts spending money, so an instruction whose first word names a
subcommand is refused as a probable typo: there is no magi run show, and
without the guard magi run show 3cbf opens worktrees and pays agents to
implement the sentence "show 3cbf". Write magi run -- show 3cbf when that is
genuinely the task.
A run branches off the base branch's tip, not off your working copy, so it
starts whether or not you have uncommitted work — and that work is not part of
the competition. magi serve would otherwise decline every task for as long as
you had something in progress, which is most of the time. magi says so in the
log when the tree is dirty, because someone watching a candidate fail to use a
change they just made deserves to know why.
The observation deck
Bare magi (or magi tui) opens every run in one screen, refreshed from disk
once a second, so a competition can be watched instead of polled:
magi 2 runs 1 active 1 done 0 attention | filter: all
┌ runs ─────────────────────────┐┌ report ────────────────────────────────┐
│> reviewing a1b2 add retries ││magi run 20260830-153012-a1b2 reviewing│
│ ready 50f1 fix the … ││ candidates │
│ ││ A opus 3 files, 2 commits <- winner
└───────────────────────────────┘└────────────────────────────────────────┘
j/k move Tab pane J/K scroll a filter r refresh o open dir ? help q quit
| key | |
|---|---|
j k ↓ ↑ |
move in the focused pane |
Tab |
switch pane (runs / report) |
J K, PageDown PageUp |
scroll the report |
g G |
newest / oldest run |
a |
cycle filter: all → active → attention → done |
r |
refresh now |
o |
open the run's directory in the OS file manager |
? |
help |
q Esc Ctrl-C |
quit |
It is read-only on purpose: the runs are the record of what the agents did,
and a keystroke that could rewrite one has no business being a j away from
browsing. Cleanup stays in magi fold.
Piped or in CI, bare magi does not raise an alternate screen — it prints the
latest run's report instead, so magi | head behaves.
Running unattended
A competition takes tens of minutes, almost all of it agent latency. Sitting in front of that is the wrong job for a human, so magi has a queue and a loop that drains it:
One task is one JSON file under <data_local>/magi/queue, so the backlog is
readable, editable, and greppable with the tools already on the machine, and a
daemon killed mid-run leaves a queue the next one picks up.
magi serve runs one competition at a time on purpose. The graph is already
parallel inside — candidates times judges — and two graphs at once doubles the
burn on the agent-CLI quota that is the real constraint.
| verb | |
|---|---|
magi task add |
file work; text, --file, --issue, or stdin |
magi task list |
the backlog, newest first |
magi task show <id> |
one task in full |
magi task hold / release |
park work, or give it a real second chance |
magi task done |
mark it finished, for work that landed by a route the loop did not see |
magi task rm |
delete it |
done and release are one keystroke apart and do opposite things. Reach for
done when the work is already in main — merged by hand, or merged by a run
that recorded its own merge as a failure. release would put the task back in
line and pay for the whole competition again to redo it.
Agents file their own work
magi task add is not a human-only command. Every agent the graph spawns gets
MAGI_RUN and MAGI_NODE in its environment, so an implementer that notices
something worth doing but out of scope can file it:
The task records implement@a1b2 as its source rather than human. That
attribution comes from the environment agent::invoke sets, which no flag can
forge by accident — which is what makes "most of the backlog was filed by
agents" a measurement rather than a claim. It is also the whole point: the CLI
is the operating surface, and the agents are its users as much as you are.
Bounded on purpose
An autonomous loop that retries forever is a way to spend money on a task that
cannot succeed. Every attempt is counted; a task that burns its attempts becomes
held and waits for a person, not for another agent.
A quota stall is refunded. When the agent CLIs hit their rate limit the
judging panel collapses, the run stops as stalled, and the task goes back in
line without spending an attempt — a quota window closing at 4am must not
leave a backlog of tasks that were never actually judged.
Only a rate limit earns that refund. A judging panel can also collapse because the judges answered with the wrong shape — which is ordinary flakiness, can recur on every attempt, and is charged to the task, so the attempt counter still bounds it. Refunding that would take the bound off the loop entirely and pay for a fresh implement wave every time.
Working out what to build, with someone
A task file without completion criteria produces a competition whose candidates
cannot be compared, and you find out forty minutes and several dollars later.
So the first step is a conversation — Chat, the standing conversation on the
phone UI (magi web):
Open Chat, talk the idea through with an agent that can read the repository
and run commands, and once the shape of the change is settled, tell it to file
the work. The agent runs magi task add --solo on your behalf — the same
command you would type yourself — and the conversation stays open, so any
number of tasks can come out of it over time rather than ending the moment one
does.
Panels: the agent formats its own confirmation screen
One line of prose is not enough to decide anything. So an agent can hand over a page it wrote itself:
The panel is the agent's own HTML and CSS — a diffstat table, a coloured diff,
a screenshot — rendered on your phone under the question. Attachments are
copied into the question, so a panel still renders after magi fold has
deleted the worktree it was written in.
Why this is safe
Rendering someone else's HTML in your browser is the thing this UI otherwise
refuses to do: nothing from the API is ever put into innerHTML, and even an
href out of a run record goes through a scheme check. A panel is the exception,
and it is only acceptable because of three things together:
- The frame is
<iframe sandbox>with no tokens at all. No JavaScript runs. It cannot reach the page around it, your cookies, orlocalStorage. There is one function in the client that builds it and a comment forbidding anyone from addingallow-scripts. - The panel is served under a strict CSP:
default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; …. Inline CSS is allowed, because formatting is the point. Everything that reaches the network is denied, so a panel cannot phone home through a remote image or a beacon — verified with the browser's own violation log. - Assets come from magi, by bare filename, from that question's own directory.
The name must match
^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$, is validated on the way in and again on the way out, and an.svgis servedContent-Disposition: attachmentso it can only ever be an<img>.
A panel that will not load says so and leaves the choices usable: you still have to be able to answer.
Approving a merge
With land_approval on — the default whenever land is on — magi asks before
it merges, and the question carries a panel with the whole case: the diffstat
as a table, the diff itself with a +/- gutter so it reads without colour,
the checks, the review comments that were addressed, the commits being squashed
and the subject the merge will use, because you are approving that too.
The diffstat comes from git diff --numstat, not --stat: the +++--- bar in
--stat is scaled to terminal width, and fabricated numbers have no business
in an irreversible decision.
Silence is a hold. An unanswered approval never merges, and neither does
anything other than the word merge.
Chat: a standing conversation, turn by turn
Chat runs as a turn-based conversation: your message, one headless agent
turn, repeat. Each turn resumes the CLI's own conversation (claude -p --resume, opencode run -s, agy --conversation), so a turn sends only your
new sentence rather than replaying the transcript and paying for it again.
Your message is written to disk before the agent is invoked, so a quota
window or a crash cannot lose something you typed.
The conversation runs with writes disabled by default: an agent that had
already edited the repository mid-conversation would make a later
competition's diffs unjudgeable. [talk] allow_write = true lifts that for a
repository that is never entered into one — a dotfiles or personal config
checkout — so a one-line fix does not have to go through the queue to land.
Unlike a one-shot interview, Chat does not end when a task is filed: the agent
runs magi task add --solo from inside the conversation, tells you the task
id, and the conversation stays open for whatever comes up next.
When an agent needs you
That is the command an agent runs, mid-task, instead of guessing. It blocks;
the question appears on your phone with the choices as buttons; the answer goes
back on the agent's stdout. From a terminal, magi answer does the same job.
A question is attributed to the seat that asked it — run 9fb7 · node implement · seat impl-A — because "who wants to know" is the first thing you need in
order to answer. Runs blocked on an unanswered question are marked as such in
the runs list: a parked run consumes nothing and progresses never, so being
noticed is the only thing that moves it.
Asking back
Answering isn't the only move. If the question doesn't make sense yet, say so instead of guessing at an answer:
(the phone's question card has the same "ask back" box). magi ask on the
agent's side returns at once — exit code 0, your words on stdout — and the
agent replies on the same question rather than starting a new one it would
have no context for:
That appends the reply and waits again. The question never leaves the card it started on — the phone shows both turns stacked under the original summary — and it counts as one open question throughout, however many times you go back and forth. The one thing that changes: while your last word is waiting on the agent, the phone locks the answer buttons and says so, because there is nothing to decide until it replies. You won't be paged again for a reply that lands within five minutes of your own message — long enough to mean "still reading" — but a slower one still notifies, in case you stepped away.
Set [notify] command to be told out of band:
[]
= ["ntfy", "publish", "magi", "{summary} — {url}"]
{summary}, {run} and {url} are substituted into the argv, never into a
shell string, so a question containing ; rm -rf stays one argument. {url}
comes from MAGI_WEB_URL, which magi web prints at startup — a run cannot
discover the address another process bound. A notification that fails is logged
and ignored: a broken webhook is not a reason to throw away an implementation.
Landing it
With merge = "pr", magi opens the pull request and then keeps going: watches
the checks, reads the review comments — human and bot — runs a fix round when
either is unhappy, pushes, and asks you to merge when they are happy.
[]
= true # default: take over the watching
= true # default: never merge without being asked
= 4
Both are on by default, and that pair is the point. land takes over the
watching an operator would otherwise do by hand; land_approval keeps the
irreversible step a human decision. An unattended merge needs both flipped, and
that has to be chosen deliberately twice.
It never force-merges: out of rounds leaves the pull request open with a
comment saying what is still failing, and checks: unknown — no signal at all —
never merges either.
A branch the base moved under is rebased, not fixed. A competition that runs for two hours against a repository merging pull requests all day will conflict on the way in, and that is arithmetic rather than a defect. magi rebases the winner onto the base as the remote has it and force-pushes with a lease. A rebase that does not apply is a decision rather than a chore: it stops and hands over what git said.
A pull request opened a second ago has no checks yet, and "not yet" is indistinguishable from "this repository has no CI". So magi waits three minutes for the checks to appear before believing there are none. Whatever it then decides, a run that got as far as opening a pull request is never re-competed: the task is held with the reason, because the implementation exists and is waiting on CI or on you. Retrying would race a second branch against your open pull request and spend the whole competition budget again.
Project conventions in the prompts
[]
= "This repo uses jj, not git. Never run `git commit`."
= "Ignore formatting; a hook owns it."
Appended to the node prompts, under a heading of their own — never merged
into them. The built-in prompts carry the invariants the competition rests
on: a judging prompt names no authors, structured answers arrive as one fenced
json block, judges are told not to speculate about authorship. A config that
could replace a prompt would let a typo un-blind the panel, and the symptom
would be "the judges got worse" rather than an error.
Repository-wide context belongs in AGENTS.md, which every agent already reads
from the checkout. These fields are for what a magi node needs to know and a
repository file cannot say.
The phone UI
The same runs, the same queue, from a phone. --bind auto (the default) finds
the machine's Tailscale address and serves there; with no Tailscale it falls
back to loopback and says so.
There is no authentication. The tailnet is the security boundary. That is a
deliberate choice for a single-operator tool on a private network, and it is the
reason the default bind is not 0.0.0.0.
It is still one binary. The interface is three files compiled in with
include_str! — no JavaScript toolchain, no CDN, no remote font, nothing
fetched at runtime. cargo install magi-cli gives you the phone UI too.
You can watch runs, read the full report, browse the queue, hold and release tasks, talk new work through in Chat, and delete a task or a finished run that is no longer wanted. Deletion is guarded rather than hidden: a task the daemon is holding a claim on, and a run a live daemon is working on right now, are refused with the reason. So is any run whose candidate worktrees have not been folded — that is the guard that keeps "delete" meaning "remove a record" rather than "throw away a worktree". A run left unfinished by a killed daemon is a leftover, not work in progress, and can be removed once it is folded.
A stalled run can be folded and resumed from the phone. Those are the two halves of "this run is in the way", and they are opposites, so the run's page offers both and says so:
- Resume carries the run on from where it stopped, re-asking only the seats whose absence collapsed the panel. The candidates are already built and paid for; the alternative an operator actually had was releasing the task, which competes three fresh implementations against work that exists. It answers 202 and runs in the background — a resume takes minutes, and a phone that locks mid-request must not lose it.
- Fold removes the run's worktrees and branches, which is what the delete button is waiting for, and is where the disk goes: three finished runs here were holding 53 GB. Folding a run makes it unresumable, which the confirmation says in as many words.
Both are refused while a live daemon is working on that run. A resume is also refused while the loop is running at all, not merely on that run: magi runs one competition at a time on purpose, and a tap that quietly started a second graph would double the quota burn for no extra throughput.
Before this the delete button greyed itself out and said "Run magi fold
first" — a phone being told to open a terminal, in the one product whose point
is that it does not need one.
Update & restart, from the phone. One tap arms it, the second replaces this binary with the newest release and brings the deck back on it:
- a run in flight parks at its next node boundary and stays resumable, so this costs at most the step it is on rather than the competition;
kaishinputs the new binary in place — it renames the running image aside, so the swap itself needs no downtime;- the server drops its listener, spawns a detached successor, and exits; the successor waits out the address and binds it;
- the loop resumes the parked run rather than competing again.
Measured end to end on a 0.1.0 deck upgrading itself to the 0.2.0 release: eleven seconds from the request to the successor answering on the same address, with no terminal involved. Refused when the loop belongs to another process, because replacing this binary would leave that one running an old binary against the same queue.
Stop, replace the binary, resume. A plain stop never abandons a run: it finishes the competition it is on, which can be an hour. That is right when you only want the queue to drain, and useless when you have fixes to install — and killing the process instead loses whatever the seats in flight had not written.
So a stop that is waiting out a run offers Park at the next step. The run stops at its next node boundary, which costs at most the node in progress because every node writes its state before the next one starts and every node skips what is already recorded. The run comes back as parked-and-resumable, the process exits, the binary can be replaced, and the next loop resumes that run rather than competing again — or you tap Resume yourself.
A park spends no attempt. It is the operator asking for the process back, not a run that failed, and a few upgrades must not exhaust a budget meant for agents that actually misbehaved.
The loop runs inside magi web. GET /api/loop reports whether it is
running and who owns it; POST /api/loop {"running": true|false} starts and
stops it. Only the process serving the page can control its own loop — a loop
started elsewhere is reported with the owning pid and both calls are refused,
because a button that silently did nothing would be worse than a refusal, and
two loops on one queue race for the same claims and bill the agent quota twice.
Answering an interview turn returns 202 immediately and runs the agent in the background: a turn takes twenty to ninety seconds, and a phone that walks behind a wall while the request is open loses the answer the server had already produced. The operator's turn is persisted before the response returns, so the transcript is never missing what you actually said.
Every list is ordered by creation, never by last activity. A list that reorders itself while you are reading it moves the row out from under your thumb.
Configuration
Config files are TOML rendered by teravars, and they deep-merge in increasing precedence:
<config_dir>/magi/config.toml < <repo>/.magi/config.toml < <repo>/magi.toml
That split exists because the roster is a machine fact — which CLIs and which
plans you pay for — while the gate is a repository fact (cargo make check
here, pnpm test there). Declare the roster once per machine and let each repo
state only what is its own. --config <path> uses that single file instead.
With no config at all, magi builds a roster from the agent CLIs on PATH.
Inside a config file you have [vars], {{ env.NAME }}, {{ system.os }},
{{ repo }}, {{ repo_name }}, and include = [...]:
[]
= "{{ env.MAGI_CACHE | default(value='/tmp') }}"
[]
= ["CARGO_TARGET_DIR={{ vars.cache }}/magi-target cargo make check"]
One trap, not two: teravars renders the raw text before TOML unescaping, so use
single quotes inside the braces (value='/tmp', never value=\"/tmp\").
Comments used to be the other one — since teravars 0.2.2 they are stripped
before Tera renders, so a comment may quote {{ ... }} or {% ... %} freely.
Tables merge; arrays do not. teravars appends arrays when it merges layers,
which is wrong for every array magi has: implementers is an ordered list of
seats, verify.gate is the commands to run, notify.command is an argv.
Concatenating two of those produces something nobody wrote — three
implementers from a machine's two and a repository's one, or an argv of
["ntfy", "publish", "curl", "-X"].
So magi refuses it. Declare any given array in exactly one layer: either
the machine states the roster and repositories override only scalars, or a
repository states its own. Naming the same array in two layers is an error that
names both files, rather than a roster you did not ask for and are paying for.
magi doctor prints what actually resolved.
It also names the states nothing will move on its own: tasks held for a
person, and tasks left running by a daemon that is no longer alive. The
second is the one worth having a line for — the loop only ever offers itself
runnable tasks, and running is not one, so a competition interrupted by a
killed daemon leaves its task sitting there forever while the summary counts
it as work in progress.
The full surface:
[[]]
= "opus"
= "claude" # claude | opencode | antigravity | codex | command
= "opus"
[[]]
= "sonnet"
= "claude"
= "sonnet"
[[]]
= "oc"
= "opencode"
# Leave a role empty to rotate through the roster. The judge seats are rotated
# by one, so judge i is never the author of candidate i.
[]
= ["opus", "sonnet", "oc"]
= ["sonnet", "oc", "opus"]
= ["opus", "oc"]
# fixer defaults to the winner's own author, continuing its own conversation.
# chatter is who Chat asks; unset picks a claude seat, else the first
# runnable agent in roster order. Name one explicitly if that agent is also a
# judge seat — Chat is opened far more often than any single competition and
# would otherwise compete with that judge for the same account.
[]
= 3
= 3
= 1
= 3
= 6
= "block" # block | warn — a round a seat never answered in
= 4
= "en" # prose language for the agents; "ja" etc.
= true
= 3600
# A re-ask that only has to restate an answer the seat already worked out gets
# a quarter of these budgets, not the whole one again.
= "~/wt/magi" # optional
[]
= true
= "warn" # warn | redact | fail
# seed = 42 # reproduce a run's label assignment
[]
# Run once per review round in the winner's worktree; failures are fed back to
# the fixer. This is the "real machine" leg of the review.
= ["cargo test --locked"]
# Final gate. Every command must exit 0 before a merge is attempted.
= ["cargo make check"]
[]
= "none" # none | local | pr
[]
= "notify" # off | notify | install
# interval = "24h"
[]
# Where `magi repos` (and `GET /api/repos`) look for other local checkouts.
# `roots` is an array, so it follows the same "declare it in exactly one
# layer" rule as `[[agents]]` - and since which checkouts exist on disk is a
# machine fact rather than a repository one, that layer is almost always the
# machine config.
= ["~/src/github.com"] # scanned as <root>/<host>/<owner>/<repo>
= 86400 # seconds a scan is trusted before re-scanning
mode = "none" is the default on purpose: magi prints the merge command and
stops. It does not touch your base branch unless you ask it to.
An arbitrary agent, or a deterministic stub for testing:
[[]]
= "mock"
= "command"
= ["sh", "./mock-agent.sh"] # {prompt_file} {cwd} {label} {session}
command agents also receive MAGI_SEAT, MAGI_TURN, MAGI_PROMPT_FILE and
MAGI_ALLOW_WRITE in the environment.
The review loop
The winner — and only the winner — enters a bounded loop:
- Each reviewer gets its own detached worktree pinned at the exact commit under review, so no reviewer can perturb the tree and the fixer never races one.
verify.e2eruns in the winner's worktree. Its output is fed to the fixer.- The fixer addresses the blocking findings, or rejects one with an argument — a rejected finding with a checkable reason is a correct outcome, and magi records it as such.
- Repeat until no blocking finding remains and verification is green, or
review_roundsis exhausted (blocked). A round where the fixer produces no commit stops the loop instead of spinning on an unchanged tree.
Finding ids (R2-1-3) are assigned by magi, never by the agent, because the
fixer's adoption report is keyed by them — that is what makes reviewer precision
measurable rather than self-reported.
A seat that never answered is not a seat that found nothing. Every round
records how many reviewers it expected against how many actually came back, so
a timeout, a crash or an unparsable reply cannot pass for a clean pass. magi show labels such a round incomplete and names the seats that went missing,
magi stats carries a per-seat timeout rate, and by default
(graph.incomplete_review = "block") the round is never treated as clean: with
nothing raised to fix, it is re-reviewed, and if the round budget runs out
while the panel is still short the run ends blocked rather than ready. Set
incomplete_review = "warn" to let the round gate on whatever did answer — the
gap stays in the report either way.
magi run and magi review exit non-zero when the run ends blocked or
stalled, after printing the report — a script or CI step that only reads
the exit code must not take "nothing panicked" for "the change is reviewed".
The one exception is a run that left a pull request open: that is a hand-off
to a human, not a failure, and exits 0.
Statistics
magi stats aggregates every run on disk:
- implementation — win rate per agent, and how often an agent produced nothing at all.
- review — findings per round, precision (adopted / submitted), unique find rate (findings no other reviewer in the same round raised, matched by normalised title or same file within five lines), and the share of its seated rounds a seat never answered in at all.
- verification — how often E2E failed while every static review was clean: the runtime defects only execution found.
These are a by-product, not a benchmark. Seats rotate, the task distribution is whatever you happened to ask for, and an agent that drew harder tasks looks worse. Read them as relative performance on your workload.
Where a run spends its time
Measured on a real run (2 candidates, 2 judges, 1 reviewer, 219 s wall):
| node | wall | spent by |
|---|---|---|
| prep | 2.8 s | git: four worktrees plus the hook |
| implement ×2, parallel | 93.0 s | the slower agent (69 s / 92 s) |
| judge ×2, parallel | 84.6 s | agent latency |
| vote | 9.2 s | agent latency |
| tally / fold / merge | 0.5 s | magi |
| review | 26.1 s | agent latency |
| e2e + gate | 1.6 s | your commands |
magi's own compute was 3.3 s of 219 s. Everything else is the agent CLIs, so the three levers that matter are:
-
max_parallel— agent processes are network-bound, so raising it is close to free. Implement, judge and review run as parallel waves. -
deliberate_rounds— deliberation is sequential by design, because a turn has to be able to answer the one before it. Each round costsjudges × turn latency. Set it to0to skip arguing and go straight to the private vote. -
verify.e2eon a compiled language — each worktree would otherwise build from scratch, which dwarfs every agent call.verifycommands run throughsh -c, so share the cache:= ["CARGO_TARGET_DIR=${TMPDIR:-/tmp}/magi-target cargo test --locked"]Only the winner's worktree runs them, one at a time, so nothing contends on cargo's lock.
Disk
candidates + judges + reviewers worktrees exist at peak (8 with the defaults).
Judge worktrees are removed as soon as the tally lands, and magi fold clears
the rest. On a large repository that is real disk; lower judges or point
worktree_root at a roomier volume.
State lives in <data_local>/magi/runs/<id>/ — run.json plus every prompt and
raw agent reply under artifacts/. MAGI_HOME moves it.
Prior art
The graph is the one described in コードを書くのもレビューも大好きだったのについに全部AIの仕事になった (yota, AGI Cockpit), reimplemented as a standalone CLI: three implementations, blind judges, deliberation on a split, private final votes, double review plus E2E behind a test gate.
License
MIT