A moderated debate between coding agents that ends in one agreed spec. You (Claude, the orchestrating session) are the moderator and synthesizer, not a debater. The runner scripts/roundtable.mjs keeps one continuous conversation per debating agent, over one of two equal transports, and recovers an agent that dies or hangs.
| Transport | How a turn runs | Memory across rounds | Default for |
|---|---|---|---|
| ACP (Agent Client Protocol) | one live session per agent; one reader per turn | the ACP session | cmd, claude, gemini, opencode |
| headless | one CLI call per turn, prompt over stdin | native resume by explicit session id (codex exec resume <id>, cmd --resume <id>, claude -p --resume <id>) |
codex (with --sandbox read-only) |
cmd and claude run headless with --permission-mode plan. gemini and opencode have no headless adapter, because their CLIs can't resume a session by id. The runner never resumes "the latest session".
0. First question: how involved does the user want to be?
Before anything else, ask through cenno (mcp__cenno__ask_user, choice):
- Hands-on: they review the disagreements after every round and steer. Run with
--mode active. - Result only: they get the final spec and are only asked if the agents deadlock or the budget runs out. Run with
--mode auto.
The answer decides what you ask next. For result only, ask nothing else that has a sensible default: pick format, agents, rounds and output yourself and state the choices in one line. For hands-on, confirm the format and agents in one mcp__cenno__ask_sequence.
1. Detect the target and write the brief
| Input | How to recognize it | What the brief contains |
|---|---|---|
| Feature in a repo | paths, a module or feature name | entry points, key files, how to run its tests |
| PR / diff / branch | PR number or URL, branch name, "my changes" | git diff base...head --stat, the base ref, the PR description |
| Task / spec | a doc, ticket or pasted text, no code yet | the spec text, constraints, what "done" means |
Write brief.md in the run directory with: Target, Type, Goal of the audit, Scope (in and out), How to verify (test commands), and Known context the agents cannot discover (business constraints, user intent). Keep it under ~150 lines and point to files instead of pasting them; the agents can read the repo.
2. Pick the format (suggest, the user can override)
Read references/formats.md. Short version:
- pingpong: 2 agents taking turns. Narrow diffs, one focused question.
- panel: N agents audit in parallel, then cross-critique. Broad features, default.
- roles: like panel, but each agent gets a lens (auditor, devil's advocate, architect, QA). Specs and designs where the premise itself is in doubt.
Default agents: codex,cmd. Add claude when a third view helps: it is a separate Claude session with its own context, but the same model family as you, so it adds no model diversity. Add opencode/gemini only when they pass doctor.
3. Check the agents
node ~/.claude/skills/agent-roundtable/scripts/roundtable.mjs doctor
doctor starts every agent over its default transport and sends a one-line prompt, so expired keys and failed logins show up here, not in round 1. The other transport is only started. Drop agents whose default transport fails and whose other transport doesn't start, and tell the user which and why. Known traps: gemini ACP needs a working Google OAuth login (gemini interactively once); opencode connects but fails at the first prompt when its API key is expired. First install: cd ~/.claude/skills/agent-roundtable/scripts && npm install.
4. Run the debate (in the background)
node ~/.claude/skills/agent-roundtable/scripts/roundtable.mjs run \
--brief RUN/brief.md --run-dir RUN --cwd TARGET_REPO \
--agents codex,cmd --format panel --rounds 3 --mode auto
Use RUN = <target repo>/docs/roundtable/<slug>-<YYYY-MM-DD>/ when the user wants a handoff doc, otherwise the scratchpad. RUN must be fresh: the runner refuses a directory that already has state.json, moderator/ or synthesis.md. Start it with run_in_background: true and watch RUN/state.json with Monitor (an until-loop on its status field), never with bare sleeps.
Options: --rounds (default 3), --min-rounds (default 2), --turn-timeout seconds per agent turn (900), --budget-min wall clock for the debate (60), --transport auto|acp|headless (auto = each agent's default), --roles a,b for the roles format, --no-veto, --drain-timeout (30) and --kill-grace (5) seconds, --registry for another agents.json.
Read-only is best effort. Agents may read anything and run commands. The ACP permission rule decides by path, never by the tool's title:
- allowed:
read,search,fetch,think,executeanywhere;edit/delete/move/otherwhen every location is outside the target and the run dir (scratch files in/tmpare fine); - denied: any change with a location inside the target or the run dir,
edit/delete/movewithout locations, a missing or unknown kind,switch_mode.
Headless agents run under their CLI's own guard (codex --sandbox read-only, cmd and claude --permission-mode plan). After every round and after the veto the runner compares a content fingerprint of the target (tracked diff plus untracked files, run dir excluded; gitignored files are not covered) and adds a warning to state.json if it changed. Report any such warning to the user.
Failed turns and degraded runs. A turn fails when the agent crashes, hangs, times out, or stops with anything other than end_turn. On a timeout the runner cancels the turn and waits up to 30 s for it to stop; otherwise it kills the agent, so a late reply can't leak into the next turn. Each agent gets 3 failed attempts over the whole run:
- restart on the same transport, with its context rebuilt from its completed
rounds/replies, and re-run the turn; - switch to headless with native resume, bootstrapped the same way;
- drop the agent.
A failed agent's vote is cleared at once. A switch or a drop makes the run degraded: state.result is degraded, state.degraded names who and what, and the consensus reason says so. Never present a degraded run as full consensus.
Run directory
| File | Written by | Meaning |
|---|---|---|
state.json |
runner | status: starting, debating, awaiting_moderator, awaiting_synthesis, veto, done, failed; waitingFor names the file it wants and waitingReason why; result: consensus, no_consensus, degraded, failed; agents.<id>: {ok, live, transport, lastError, failedTurns, nativeSessionId}; turns: every attempt with transport, duration, outcome, stopReason; degraded, deadlocks, warnings |
rounds/rN-<agent>.md |
runner | each agent's reply; .prompt.md is what it was sent |
rounds/rN.summary.json |
runner | parsed status blocks and the consensus verdict |
moderator/rN.md |
you | note injected into the next round (each note is read once). First line STOP ends the debate |
synthesis.md |
you | your merged draft; triggers the veto round |
Write every file you hand to the runner atomically: write moderator/rN.md.tmp (or synthesis.md.tmp), then mv it to the final name. The runner reads only the final name.
| veto/<agent>.md, veto.json | runner | each agent's approve/object on the draft |
| events.log | runner | transport choices, tool calls, permission decisions, failures |
5. Moderate
When status is awaiting_moderator (active mode), read the round's replies and summary, then:
- Show the user the disagreements only (finding id, who holds what, the evidence) through cenno and ask how to steer.
- Write their direction plus your own procedural notes to
moderator/rN.md. Moderate the process (which disagreements to settle, what evidence is missing), do not argue a side.
In auto mode the runner detects deadlock itself: when the same set of blocking ids holds two rounds running, it switches to awaiting_moderator (with waitingReason: deadlock …) even in auto mode. Read the two rounds, ask the user through cenno if it needs a human call, and write moderator/rN.md: which disagreement to settle and what evidence would settle it, or STOP. You may also drop a note in auto mode without being asked; it is picked up at the start of the next round.
6. Synthesize, then get the veto
When status is awaiting_synthesis, write synthesis.md using references/output-template.md. Rules that keep your bias in check:
- Every finding that any agent still holds as blocking appears in Open disagreements, with each agent's position in their own terms. Never resolve a disagreement by leaving it out.
- Attribute findings to the agent ids that raised them.
- Do not add findings of your own in the main lists; put them under "Moderator notes" so the agents can veto them.
- If the run is degraded, name the switched or dropped agents and say what that means for the result (a dropped agent did not vote on the final picture).
The runner sends the draft to every live agent, together with every agent's final-round status and a short summary of its last reply, so a veto is judged against where the debate actually ended. If veto.json has an objection, fix what it points to (or move it into Open disagreements), write a short note of what changed, and finalize. Do not run a second veto round unless the user asks.
7. Deliver
Ask (or, in result-only mode, use what the user said earlier) where the result goes:
- Handoff document: the final
synthesis.mdsaved asRUN/final.mdin the target repo, linked in your reply. - Chat: the final spec, task list and open disagreements in the reply itself.
Always end with: agents and transports used (and any switch or drop), state.result, rounds run and why it stopped (state.stopReason), veto results, and any warnings.
Configuration
- Agents live in
agents.json: defaulttransport,acpcommand,headless.start/headless.resumeargv with{out},{cwd},{session},{promptFile}placeholders,input(stdin or file) andoutput(codex-jsonl, json), optionalauthMethod(used only whensession/newfails with the auth-required code and the agent advertises that method). Add an ACP agent by adding an entry. - Tests:
cd scripts && npm test. They drive the real ACP SDK against a fake agent (scripts/test/fake-agent.mjs) and a fake resumable CLI. - The consensus rule is
scripts/consensus.mjs,assessConsensus(statuses, ctx). - Prompt templates and the
roundtable-statusblock format are inscripts/prompts.mjs.