Codex ⇄ Claude debate
This skill runs an automated debate between codex and Claude that loops to
consensus with no round cap and no deadlock exit. It has two modes, selected by
an explicit leading subcommand — never by guessing from the argument's shape:
review — codex reviews the current diff, a Claude author fixes/disputes,
round after round until they agree, and the trail is committed + posted to the
PR (a mutating, outward-facing mode). This is everything from
Review mode down.
answer — Claude and codex each answer a freeform prompt in parallel,
then cross-check each other until both agree, and a unified answer is
returned to you (read-only; plus a saved transcript). See
Answer mode.
The two modes have different side-effect contracts (review mutates + writes to
the PR; answer is read-only), so the mode is chosen explicitly, not inferred
from whether the argument looks like a PR number or like prose. Inferring a
mutating action from prose shape is exactly the coupling this design avoids.
Mode detection (do this first)
Look at the first whitespace-delimited token of $ARGUMENTS:
answer → answer mode. The prompt is everything after the answer
token. Jump to Answer mode; the review-mode steps do not apply.
review → review mode. The remaining args are the review grammar
([<pr-number>] [--base …] [--no-commit] [--no-comment] [--rationale <note>] [--context <note>]). Continue with Review mode.
- No args, OR the first token is a number (a PR number) or a
--flag →
review mode (the backward-compatible bare alias for the original
/codex-debate [<pr>] [flags], so existing callers like /be-review keep
working). Continue with Review mode.
- Anything else (freeform prose with no recognized subcommand) → ambiguous.
Do not guess — ask the user to pick an explicit mode and stop, e.g.: "Did you
mean
/codex-debate answer \"<your prompt>\" (read-only) or /codex-debate review [<pr>] [flags] (mutating)?" Only the safe, backward-compatible review grammar
auto-routes; prose never silently triggers a mode.
Both modes require Claude Code's Workflow tool (the engine). Under
codex/opencode runtimes the skill is inert.
Review mode — Codex ⇄ Claude review debate
Automate the back-and-forth you'd otherwise courier by hand: codex (the
reviewer) critiques the current change, a Claude subagent (the author)
fixes what it agrees with and disputes what it doesn't, codex re-reviews, and so
on — round after round, until they reach consensus. codex reviews from a
warm session: round 1 cold-starts the reviewer, and every later round
resumes that same codex session (codex exec resume), so codex carries its own
prior review and reasoning forward instead of reconstructing it from the diff +
rebuttal each round — when Claude disputes a finding, codex argues from its
original rationale. There is no round cap and
no "deadlock" surrender: a debate that quits without agreement defeats the
purpose, so the two sides keep arguing until one concedes. You stay out of the
middle: each round lands as its own commit whose
message carries the debate context (codex's findings + Claude's dispositions) so
the PR history reads as the debate, and the summary is posted to the PR as a
comment at the end.
Why this shape
The two sides are asymmetric, and that asymmetry is the whole design:
- codex is CLI-invokable headlessly (
codex exec, authed via ChatGPT), so it
runs from a shell command.
- Claude on a Max plan is not headless —
claude -p doesn't work with Max
auth. But the Workflow tool's agent() spawns Claude subagents through the
harness, not claude -p, so it works. That subagent is the author side.
So the debate runs as a Workflow: agent() is Claude, a Bash-invoked
codex exec is the reviewer, and the script couriers structured verdicts
between them and decides when they agree. Both sides are forced to emit
schema-constrained JSON, so consensus is detected in code, not by vibes.
This skill requires Claude Code's Workflow tool (it is the engine). Under
codex/opencode runtimes the skill is inert.
Arguments
A leading review subcommand token, if present, is consumed by mode detection;
what remains is [<pr-number>] [--base <branch>] [--no-commit] [--no-comment] [--rationale <note>] [--context <note>] (the bare alias passes the whole argument
string through unchanged). Parse:
<pr-number> (optional): a PR to debate. If given, gh pr checkout <n>
first and default the base to that PR's base branch. If omitted, debate the
current branch's working-tree diff.
--base <branch>: ref to diff against. Always a remote-tracking ref, never
a stale local branch. Default: origin/<PR base> when a PR number is given, else
the repo default branch as git symbolic-ref --short refs/remotes/origin/HEAD
(e.g. origin/master) — used as-is, NOT stripped to local master (which
can lag the remote). Fallback origin/master. Step 1 runs git fetch origin
first so the ref is current. The workflow then resolves this to the merge-base
of base and HEAD and diffs against that, so commits base gained since the
branch forked aren't reviewed as part of this change.
--no-commit: don't commit per round — leave all agreed changes
uncommitted in the working tree for you to commit yourself. Default is to
commit each round (see below).
--no-comment: don't post the debate summary to the PR. By default, when
a PR exists, the debate summary IS posted as a PR comment (see step 3). Pass
this to suppress the outward-facing write and report in chat only.
--rationale <note> (optional): the author's note on deliberate design
decisions. Threaded into both sides — codex's round-1 review prompt (so it
doesn't flag intentional choices as defects; its warm session carries the note
across later rounds) and the Claude author's prompt every round (so it
disputes, rather than "fixes", a finding that contradicts a deliberate choice).
Mirrors /lens-debate's rationale. Pull it from the PR/issue description, or the
caller (/be-review) passes the change rationale straight through.
--context <note> (optional): the main-agent context the Claude author
should inherit — what this change is FOR (its task/intent and key decisions the
orchestrator already holds). Injected into the author's prompt every round so it
no longer reconstructs intent from the diff alone (agent() is one-shot and can't
be resumed the way codex is, so re-injection is how it inherits at all). Given to
the author only, not codex — codex stays an independent reviewer of the code,
not the author's narrative. /be-review passes the task context through.
Steps
1. Resolve context
- Determine
repoPath (the worktree root, normally the cwd).
git fetch origin so remote-tracking refs are current — the base is an
origin/... ref, and a stale one would diff against the wrong tree.
- Resolve
base per the rules above (a remote-tracking ref like origin/master).
- If a PR number was given,
gh pr checkout <n> and confirm the branch.
- Confirm there is a non-empty diff:
git diff --stat <base>. If empty, tell the
user there's nothing to review and stop.
- Preflight codex:
codex login status. If not logged in, stop and tell the
user to run codex login (suggest the ! prefix to do it in-session).
2. Run the debate Workflow
Invoke the Workflow tool pointing at this skill's committed script, passing
context through args:
Workflow({
scriptPath: ".claude/skills/codex-debate/debate.workflow.js",
args: {
repoPath: "<worktree root>", // also the per-worktree scratch dir root
base: "<base branch>",
commit: <false only if --no-commit>,
skillDir: ".claude/skills/codex-debate",
context: "<main-agent context the author inherits; omit/'' if none>",
rationale: "<author's note on deliberate decisions; omit/'' if none>"
}
})
The workflow runs in the background and notifies you when it completes. It
alternates codex:roundN and claude:roundN agents under a Debate phase —
the user can watch live via /workflows. Each Claude round edits the working
tree and (unless --no-commit) commits exactly that round's changed files in
the same session — one commit per round, with a message embedding the round's
codex findings and Claude's dispositions — never pushing or merging. (The commit
is no longer a separate commit:roundN agent: the author already has the tree
open, so it commits its own round.)
Ephemeral scratch (verdicts, the debate ledger) lives under the gitignored,
per-worktree <repoPath>/.codex-debate/, so parallel debates in different
worktrees never collide and the scratch never shows up in the diff codex
reviews. It returns:
{ status: "consensus" | "commit-incomplete" | "section-incomplete" | "reviewer-error",
rounds, base, finalVerdict, filesChanged, commitGaps, sectionGaps, transcript,
commentHeader, // the comment's small deterministic header (badge + round count + effort + base)
workDir, sectionGlob } // where the per-round section files live; cat them under the header (step 3)
(each transcript[] round also carries a commit SHA when that round committed;
commitGaps lists the round numbers whose author edited files but returned no
commit SHA — empty unless status === "commit-incomplete"; sectionGaps lists the
round numbers whose author left no disposition section file — empty unless
status === "section-incomplete".)
There is one more, earlier terminus the workflow can return before the
debate loop even starts — merge-base-error. If git merge-base <base> HEAD
fails (a missing/typoed/stale base, or unrelated history), the diff scope can't be
trusted, so the workflow aborts up front rather than review the base branch's
drift as if this change made it. It returns a different, smaller shape — no
commentHeader, workDir, or sectionGlob, because no debate (and so no section
files) ever ran:
{ status: "merge-base-error", base, rounds: 0, transcript: [], finalVerdict: null,
note } // human-readable: which base failed and how to fix it (e.g. `git fetch`)
The debate is recorded as small Markdown section files under <workDir> — two
per round: section-NNN-1-codex.md (codex's verdict + findings) and
section-NNN-2-claude.md (the author's per-finding dispositions), zero-padded so
the section-*.md glob sorts in chronological order. Each file is written by the
party that owns its content: a Haiku writer renders codex's small structured
verdict to disk, and the author writes its own dispositions directly — the same
way codex writes its verdict to a path. That author-writes-its-own-file design is
deliberate: it keeps the author's structured return a minimal ack
(filesChanged/commitSha/done), so a big multi-finding narrative can never
overflow the structured-output encoding (the failure that used to crash the debate
on large diffs). The author's disposition file does triple duty — it's the author's
cross-round memory, the rebuttal codex reads next round (codex-review.sh
cats it straight into codex's prompt), and the published comment (step 3 cats
the section files under commentHeader). So the comment is still a deterministic
render — a shell cat, never re-improvised through an agent, nothing weak ever
retyping a large blob. codex is not a memory reader — it keeps its own warm
session and only ever reads the one rebuttal file.
- consensus — every finding codex raised is resolved (any severity — Claude
fixed it or codex conceded the dispute). This is the only way the debate ends
normally: it keeps running rounds until codex and Claude agree on every point,
with no round cap and no deadlock exit. (The harness's own
per-workflow agent backstop is the sole hard ceiling; if you ever need to stop
a debate by hand, interrupt it via
/workflows or TaskStop.)
- commit-incomplete — the debate converged (codex approved, nothing open),
but a round's author edited files yet returned no commit SHA, so its
in-session commit didn't land and the "one commit per round" contract broke for
the round(s) in
commitGaps. The edits are not lost — they stay in the
working tree and the next reviewer diffs them against the base — but this is
not a clean consensus: a human must reconcile the uncommitted round(s)
(e.g. commit the outstanding tree) before relying on the per-round history. Do
not report it as a plain consensus (see step 3).
- section-incomplete — the debate converged, but a round's author skipped
or under-filled its disposition section file (
section-NNN-2-claude.md missing,
empty, or missing a backticked marker for an open finding, for the round(s) in
sectionGaps). That file is the hole-free trail everyone draws on —
the author's memory, the rebuttal codex reads next round, and part of the posted
comment — so a miss means the published record has a gap. The code guards against
feeding an empty rebuttal to codex (it warns and keeps the prior pointer), and the
tree edits are still present, but this is not a clean consensus: a human must
fill in the missing round(s) before trusting the per-round record. Do not
report it as a plain consensus (see step 3).
- merge-base-error — an up-front abort, before any debate round runs:
git merge-base <base> HEAD failed (missing/typoed/stale base, or unrelated
history), so the review scope can't be trusted. The return carries a human-readable
note (which base failed, how to fix it — e.g. git fetch) and none of the
comment-assembly fields (commentHeader/workDir/sectionGlob), because no
section files were ever written. Report the scope failure and skip comment
assembly/posting entirely (see step 3); fix the base ref and re-run.
- reviewer-error — the one abnormal terminus: codex itself failed to
produce a verdict (broken/unavailable CLI), so the workflow synthesized an
error verdict and aborted rather than spin forever on a dead reviewer. This is
infrastructure failure, not a debate outcome —
finalVerdict.summary
carries the failure detail (including how many attempts were made). Do not
treat it as consensus (see step 3). Transient failures are retried first:
codex-review.sh retries the codex exec invocation with linear backoff
(default 3 attempts; tune via CODEX_REVIEW_RETRIES / CODEX_REVIEW_BACKOFF)
and only synthesizes the reviewer-error verdict once every attempt comes back
empty — so a single codex hiccup no longer sinks the round.
3. Present the result
First branch on status. If status === "merge-base-error", the workflow
aborted before any debate ran — git merge-base <base> HEAD failed, so the
review scope couldn't be trusted. This return has no commentHeader,
workDir, or sectionGlob (no section files exist), so there is nothing to
assemble: do not run the posting block below. Report the scope failure —
surface the return's note (it names the failing base and the fix) — tell the
user to repair the base ref (e.g. git fetch, fix a typo'd/stale ref) and re-run,
and skip the rest of this section.
If status === "reviewer-error", the debate did
not reach consensus — codex never produced a real verdict. Report it as a
failure, not a success: surface finalVerdict.summary (and the workflow log)
so the user sees codex was broken/unavailable, and tell them to fix codex (e.g.
codex login, check the CLI) and re-run. Do not post a consensus badge or a
## Codex ⇄ Claude debate PR comment for this path — there is no agreement to
report. Skip the rest of this section.
If status === "commit-incomplete", the debate converged but at least one round
left its edits uncommitted (the round numbers are in commitGaps). Report it
as converged-but-not-clean: assemble and post the comment (see the posting
block below — commentHeader already shows a ⚠️ badge, not the consensus
check), then tell the user which round(s) are uncommitted and that the outstanding
tree must be committed before the per-round history can be trusted. Do not call
it a clean consensus.
If status === "section-incomplete", the debate converged but at least one round's
author skipped or under-filled its disposition section file (missing, empty, or
omitting a marker for an open finding; round numbers in sectionGaps).
Report it as converged-but-not-clean: assemble and post the comment as usual
(the ⚠️ badge is already set), then tell the user which round(s) are missing
their disposition record and that the per-round history has a gap a human should
fill before trusting it. Do not call it a clean consensus.
Otherwise (status === "consensus") report in chat (do not push or merge —
the per-round commits sit on the local branch for the human to review):
The outcome — consensus — and how many rounds it took to get there.
The reviewer's reasoning effort — sourced from the workflow's single
REASONING_EFFORT constant (xhigh today), which is passed down to
codex-review.sh's -c model_reasoning_effort and into the comment header, so
the published value and the config codex actually ran at share one home. Read
it off the header rather than asserting it independently. State it so the depth
of the review is on the record.
git log --oneline <base>..HEAD (the per-round debate commits) and
git diff --stat <base> so the user sees what the debate changed.
A compact per-round summary — read it straight from the section files
(cat <workDir>/section-*.md: each round's codex verdict, then the author's
dispositions and commit SHA) so the convergence reads round by round. No need to
re-derive it from transcript; the sections already render it.
The agreed changes are committed per round on the local branch (or, under
--no-commit, uncommitted in the working tree). The user reviews, then pushes
/ merges (or runs /do --from post-implement) when satisfied.
Post the debate summary to the PR (default). When a PR exists and
--no-comment was NOT passed, assemble the comment from the workflow's
return — the small commentHeader, then the per-round section files cat-ed in
glob order — and gh pr comment <pr> -F <file>:
mkdir -p "$workDir" # reviewer-error/--no-commit runs may not have created it
{
printf '%s\n' "$commentHeader"
for f in "$workDir"/section-*.md; do printf '\n'; cat "$f"; printf '\n'; done
} > "$workDir/comment.md"
gh pr comment <pr> -F "$workDir/comment.md"
($workDir is the returned workDir, i.e. <repoPath>/.codex-debate; the
for-loop guarantees a blank line between sections regardless of each file's
trailing newline.) The result is the ## Codex ⇄ Claude debate header (consensus
badge, round count, the reasoning-effort note from the workflow's
REASONING_EFFORT constant) followed by the per-round breakdown of codex's
findings and the author's dispositions — the same section files the author
read as memory and codex read as the rebuttal. So the comment is a
deterministic shell concat of the record everyone drew on, not an
LLM-improvised table — nothing weak ever retypes a blob. This is an outward-facing
write — on by default because the whole point is to leave the review trail on the
PR; --no-comment suppresses it.
Answer mode — Codex ⇄ Claude answer debate
When the argument is a freeform prompt (not a PR number/flags), the skill
generalizes the same debate machinery from reviewing a diff to answering a
question. The shape is symmetric, not author⇄reviewer: Claude and codex are
two equal peers. They each answer the prompt independently and in parallel,
then cross-check each other's answer round after round — conceding where the
other is right, holding firm (with evidence) where it isn't — until both agree.
A final pass synthesizes their two converged answers into one unified reply,
which you present to the user along with a saved transcript.
Both peers are codebase-aware but read-only: each may read this repo (git diff/log, read files, grep) to ground its answer, but neither edits anything —
codex stays under --sandbox read-only (kernel-enforced), and the Claude peer is
instructed not to write. Consensus is schema-detected in code: each side emits
a structured answer with an agreesWithOther boolean and an objections list, and
the loop ends only when both sides report no remaining disagreement. There is
no round cap and no deadlock exit — same as review mode.
Steps
A1. Resolve context
- Determine
repoPath (the worktree root, normally the cwd).
- Capture the prompt: everything after the
answer subcommand token (strip
surrounding quotes). If it's empty, ask the user what they want answered and stop.
- Preflight codex:
codex login status. If not logged in, stop and tell the
user to run codex login (suggest the ! prefix to do it in-session).
- No
git fetch / base resolution / gh pr checkout here — answer mode doesn't
diff a branch.
A2. Run the answer Workflow
Invoke the Workflow tool pointing at this skill's committed answer script,
passing the prompt through args:
Workflow({
scriptPath: ".claude/skills/codex-debate/answer.workflow.js",
args: {
repoPath: "<worktree root>", // also the per-worktree scratch dir root
prompt: "<the user's freeform prompt, verbatim>",
skillDir: ".claude/skills/codex-debate"
}
})
The workflow runs in the background and notifies you when it completes. It runs an
Answer phase (round 1: claude:round1 and codex:round1 in parallel), a
Reconcile phase (rounds 2+: each side cross-checks the other, in parallel,
round after round), and a Synthesis phase that merges the two agreed answers.
Watch live via /workflows. Ephemeral scratch (per-side answers, cross-check
files, per-round sections, the saved transcript) lives under the gitignored,
per-worktree <repoPath>/.codex-debate/, so parallel debates never collide. It
returns:
{ status: "consensus" | "reviewer-error" | "agent-error" | "synthesis-error" | "no-prompt",
rounds, prompt, finalAnswer, transcriptPath, reasoningEffort, codexError }
- consensus — the only normal terminus: both sides agreed and then both
approved the synthesized candidate (see the convergence note), and
finalAnswer is that approved unified answer. transcriptPath points at the saved
Markdown transcript (.codex-debate/answer-<slug>.md).
- reviewer-error — codex itself failed to produce an answer (broken/unavailable
CLI) after retries;
codexError carries the failure detail. Infrastructure
failure, not a debate outcome.
- agent-error — one side died on a terminal API error after retries.
- synthesis-error — both sides DID agree, but the final synthesis pass produced
no answer (the synthesis agent died or returned empty). Not a successful answer —
report it as a failure (there is agreement on record, only the merge failed).
- no-prompt — the prompt was empty (shouldn't happen if A1 guarded it).
A3. Present the result
- If
status === "consensus": present finalAnswer to the user as the answer
— this is the unified reply both Claude and codex agreed on. State how many
rounds it took to converge and that codex answered at reasoningEffort
(read it off the return value). Point the user at the saved transcript
(transcriptPath) for the full convergence trail; optionally cat the
.codex-debate/answer-section-*.md files to show a compact per-round summary
(each side's answer, what changed, remaining objections). This mode makes no
outward-facing writes — no PR comment, no commits — it just answers.
- If
status !== "consensus": report it as a failure, not an answer. Surface
codexError (for reviewer-error) or the workflow log so the user sees what
broke, and tell them how to fix it (e.g. codex login) and re-run. Do not
present a half-debate as if it were an agreed answer.
Answer-mode safety & notes
- Both peers read-only — but enforced ASYMMETRICALLY. codex runs under
--sandbox read-only (kernel-enforced, belt-and-suspenders with the prompt text —
it reads arbitrary repo files and could be prompt-injected). The Claude peer is
only prompt-enforced: the harness's agent() exposes no sandbox/tool restriction
(the same is true of every Claude reviewer in /lens-debate and review mode), so
Claude's read-only behaviour rests on instruction, not a kernel guard. A
prompt-injected or mistaken Claude agent could in principle edit files or run a
git write — answer mode does not, and cannot here, harden against that the way it
does for codex. If that risk matters for a given prompt, run the debate in a
disposable/read-only worktree. Treat the read-only guarantee as hard for codex,
best-effort for Claude.
- Warm codex session. Round 1 cold-starts
codex exec; every later round
resumes the same session (codex exec resume) so codex cross-checks from its own
prior answer rather than reconstructing it. The session id lives in the
gitignored per-worktree .codex-debate/ (a distinct codex-answer-session.id,
so it never collides with review mode's session), degrading gracefully to a cold
start if capture ever fails.
- Symmetric convergence, schema-detected, candidate-confirmed. Each side emits
agreesWithOther + objections; a side counts as agreeing only when it sets
agreesWithOther:true AND leaves no objection, so a stray objection can't be
papered over by an over-eager boolean. Because the two run in parallel each round,
a single mutually-agreeing round can be a swap false positive (Claude adopts
codex's prior answer while codex adopts Claude's — both report agreement, but their
current outputs are swapped and still differ), and they can keep swapping back and
forth, so counting consecutive parallel agreements does not prove the current
outputs match. The only sound test is to make both sides judge one shared piece
of text. So when a round shows mutual agreement, the workflow synthesizes a single
candidate from the two agreed answers and runs a confirmation phase: both
sides review that identical candidate (without rewriting their own answer) and
either approve it or object. Approval is on one fixed text both actually saw, so no
swap is possible; if both approve, that candidate is the converged answer — already
signed off by both debaters (which is also why finalAnswer is never unapproved
synthesized text). If either objects, the candidate is dropped and the cross-check
loop resumes with the objections folded in. No round cap, no deadlock exit.
- Chat + saved transcript, no outward writes. The unified answer is presented
in chat and the full transcript is saved to the gitignored
.codex-debate/answer-<slug>.md. Unlike review mode, answer mode never commits
or posts to a PR.
Safety & notes (review mode)
- codex runs read-only — enforced, not just asked. codex is invoked with
--sandbox read-only, so the kernel sandbox blocks file writes and other
state-mutating syscalls; the prompt's "don't write" instruction is belt-and-
suspenders, not the only guard. This matters because codex reviews arbitrary
diffs and could be prompt-injected by file contents. The only writes to the
tree come from the Claude author rounds. (codex auto-falls-back to its bundled
bubblewrap when the system one is absent, so read-only works in containers.)
Resume rounds enforce the same read-only policy via -c sandbox_mode=read-only
(the resume subcommand has no --sandbox flag) — same kernel guard, set
through config instead of the flag.
- Warm reviewer session. Round 1 cold-starts
codex exec; the runner records
codex's session id (its thread_id, captured from the --json event stream)
under the scratch dir and every later round codex exec resumes it, so codex
retains its own prior review across rounds. The session id lives in the
gitignored per-worktree .codex-debate/, so parallel debates never resume each
other's sessions. If the id is ever missing (round-1 capture failed), a later
round transparently cold-starts with the full prompt + rebuttal — graceful
degradation, never a wedge.
- Warm author (context, not session). The Claude author can't be resumed the
way codex is —
agent() is one-shot and Claude isn't headless under Max auth,
so there's no session id to carry forward. The equivalent is context, not state:
each follow-up round the author reads the per-round section files
(cat .codex-debate/section-*.md) — every prior round's codex findings and its
own dispositions — so it builds on its last round rather than re-deriving the
whole diff, and won't re-fix or re-litigate findings already settled. Each round
writes two small files (a Haiku-rendered codex section and the author's own
disposition section), so round N>1 always sees rounds 1..N-1; round 1 has none
yet, so it's byte-identical to a cold start (and if no sections exist, the author
falls back to the diff + verdict). The same sections compose the PR comment step
3 posts and the rebuttal codex reads, so the author's memory, the published
summary, and codex's rebuttal are one record. Crucially, the author writes its
own disposition section directly — it never has to pour that narrative through a
structured field — so its structured return stays a minimal ack and can't overflow
on a large, many-finding diff (the failure this design replaced). The Haiku writer
only ever renders codex's small structured verdict, so nothing weak retypes a
large blob. codex stays on its own warm session and never reads the sections —
only the one rebuttal file each round.
- Inherited context, not just diff. On top of that cross-round memory, when the
caller passes
context and/or rationale the author inherits them in EVERY
round's prompt — the main-agent intent (what the change is FOR) and the
deliberate-decision note. So even round 1 reasons from the change's purpose
rather than the diff alone, and the author disputes a finding that contradicts a
deliberate choice instead of dutifully "fixing" it. The rationale also rides
codex's round-1 prompt (see codex-review.sh), so the reviewer doesn't raise those
intentional choices at the source; context is the author's alone (codex stays an
independent reviewer of the code, not the narrative).
- Commits, but never pushes or merges. Each round is committed locally (unless
--no-commit) so the PR history reads as the debate, but the skill never
pushes or merges. Consensus means "both AIs agree on the committed code," not
"ship it" — the human reviews the commits and pushes/merges.
- Parallel-safe. Ephemeral scratch (verdicts and the per-round section files,
the author-written ones doubling as the rebuttal) lives under the gitignored,
per-worktree
<repoPath>/.codex-debate/, so debates on many worktrees run at once
without clobbering each other — no shared /tmp paths, and each worktree's section
files are its own.
- Posts to the PR by default. When a PR exists, the debate summary — the
commentHeader followed by the per-round section files cat-ed together (step 3)
— is posted as a PR comment (outward-facing write) unless --no-comment is passed
— the point is to leave the review trail on the PR.
- Runs to consensus — no cap, no deadlock exit. The loop ends only when codex
and Claude agree; it does not bail out at a round cap or declare a "deadlock," because
a debate that quits without agreement is pointless. The two sides keep arguing
until one concedes. The harness's own per-workflow agent backstop is the sole
hard ceiling; interrupt via
/workflows or TaskStop if you ever need to stop
one by hand. The one carve-out is not a deadlock exit: a finding that is not a
code edit for this worktree but a downstream / ship-phase / process gate (a companion
repo pinning this repo's final post-review HEAD, a CI/release step, a cross-repo PR)
cannot be satisfied during the review — it targets the post-gauntlet HEAD. When
CLAUDE shows a finding is such a gate, codex marks it resolved-and-deferred
(acknowledged, handed to the ship phase) instead of holding it open forever. The CODE
debate still converges to consensus the normal way; this only stops the loop spinning
on a process gate neither side can land mid-review. It is narrow by design — a genuine
code defect CLAUDE simply dislikes is still argued to consensus, no exit. (This is the
loop that once spun until a human killed it on a @kolu/surface cross-repo run.)
Files
Shared:
scripts/codex-exec-lib.sh — the sourced core both modes share: the read-only
codex exec/resume invocation, warm-session resolve/persist, retry/backoff,
thread-id capture, and the synthesized error-verdict fallback (via a caller hook).
The two mode scripts source this and add only their own prompts + verdict shape.
Review mode:
debate.workflow.js — the Workflow script (the loop + consensus logic).
scripts/codex-review.sh — the review-specific invocation (arg parsing, the
review prompts, the verdict schema/session file, the verdict-shaped error).
scripts/codex-verdict.schema.json — the JSON Schema codex's verdict is constrained to.
Answer mode:
answer.workflow.js — the Workflow script for the symmetric answer-debate
(parallel answers → cross-check loop to agreement → synthesis).
scripts/codex-answer.sh — the answer-specific invocation (arg parsing, the
answer prompts, the answer schema/session file, the answer-shaped error).
scripts/codex-answer.schema.json — the JSON Schema codex's answer is constrained to.
These are generated from agents/.apm/skills/codex-debate/; edit the source there and
run just ai apm to regenerate.
ARGUMENTS: $ARGUMENTS
1---2name: codex-debate3description: Run an automated codex⇄Claude debate to consensus — no round cap, no deadlock exit. Two explicit subcommands. `review` (also the bare/back-compat default) — codex (reviewer) critiques the current diff and a Claude subagent (author) fixes/disputes, looping until they agree. `answer` — Claude and codex each answer a freeform prompt in parallel, then cross-check until they agree, and a unified answer is returned. Use when the user types `/codex-debate`, asks to "have codex review this", "run the codex debate", "review this PR with codex", "argue this with codex until you agree", or passes a question to "have Claude and codex debate/answer until they agree".4---56# Codex ⇄ Claude debate78This skill runs an automated debate between **codex** and **Claude** that loops to9consensus with no round cap and no deadlock exit. It has **two modes**, selected by10an **explicit leading subcommand** — never by guessing from the argument's shape:1112- **`review`** — codex reviews the current diff, a Claude author fixes/disputes,13 round after round until they agree, and the trail is **committed + posted to the14 PR** (a mutating, outward-facing mode). This is everything from15 [Review mode](#review-mode) down.16- **`answer`** — Claude and codex **each answer a freeform prompt in parallel**,17 then **cross-check each other** until both agree, and a **unified answer** is18 returned to you (read-only; plus a saved transcript). See19 [Answer mode](#answer-mode).2021The two modes have **different side-effect contracts** (review mutates + writes to22the PR; answer is read-only), so the mode is chosen **explicitly**, not inferred23from whether the argument looks like a PR number or like prose. Inferring a24mutating action from prose shape is exactly the coupling this design avoids.2526## Mode detection (do this first)2728Look at the **first whitespace-delimited token** of `$ARGUMENTS`:2930- **`answer`** → **answer mode**. The prompt is everything after the `answer`31 token. Jump to [Answer mode](#answer-mode); the review-mode steps do not apply.32- **`review`** → **review mode**. The remaining args are the review grammar33 (`[<pr-number>] [--base …] [--no-commit] [--no-comment] [--rationale <note>]34 [--context <note>]`). Continue with [Review mode](#review-mode).35- **No args, OR the first token is a number (a PR number) or a `--flag`** →36 **review mode** (the backward-compatible bare alias for the original37 `/codex-debate [<pr>] [flags]`, so existing callers like `/be-review` keep38 working). Continue with [Review mode](#review-mode).39- **Anything else** (freeform prose with no recognized subcommand) → **ambiguous**.40 Do **not** guess — ask the user to pick an explicit mode and stop, e.g.: "Did you41 mean `/codex-debate answer \"<your prompt>\"` (read-only) or `/codex-debate review42 [<pr>] [flags]` (mutating)?" Only the safe, backward-compatible review grammar43 auto-routes; prose never silently triggers a mode.4445Both modes require Claude Code's **`Workflow` tool** (the engine). Under46codex/opencode runtimes the skill is inert.4748<a id="review-mode"></a>49# Review mode — Codex ⇄ Claude review debate5051Automate the back-and-forth you'd otherwise courier by hand: **codex** (the52reviewer) critiques the current change, a **Claude subagent** (the author)53fixes what it agrees with and disputes what it doesn't, codex re-reviews, and so54on — round after round, **until they reach consensus**. codex reviews from a55**warm session**: round 1 cold-starts the reviewer, and every later round56*resumes that same codex session* (`codex exec resume`), so codex carries its own57prior review and reasoning forward instead of reconstructing it from the diff +58rebuttal each round — when Claude disputes a finding, codex argues from its59original rationale. There is no round cap and60no "deadlock" surrender: a debate that quits without agreement defeats the61purpose, so the two sides keep arguing until one concedes. You stay out of the62middle: each round lands as its own commit whose63message carries the debate context (codex's findings + Claude's dispositions) so64the PR history reads as the debate, and the summary is **posted to the PR** as a65comment at the end.6667## Why this shape6869The two sides are asymmetric, and that asymmetry is the whole design:7071- **codex** is CLI-invokable headlessly (`codex exec`, authed via ChatGPT), so it72 runs from a shell command.73- **Claude on a Max plan is *not* headless** — `claude -p` doesn't work with Max74 auth. But the **Workflow tool's `agent()` spawns Claude subagents through the75 harness**, not `claude -p`, so it works. That subagent is the author side.7677So the debate runs as a Workflow: `agent()` is Claude, a Bash-invoked78`codex exec` is the reviewer, and the script couriers structured verdicts79between them and decides when they agree. Both sides are forced to emit80schema-constrained JSON, so consensus is detected in code, not by vibes.8182**This skill requires Claude Code's `Workflow` tool** (it is the engine). Under83codex/opencode runtimes the skill is inert.8485## Arguments8687A leading `review` subcommand token, if present, is consumed by mode detection;88what remains is `[<pr-number>] [--base <branch>] [--no-commit] [--no-comment]89[--rationale <note>] [--context <note>]` (the bare alias passes the whole argument90string through unchanged). Parse:9192- **`<pr-number>`** (optional): a PR to debate. If given, `gh pr checkout <n>`93 first and default the base to that PR's base branch. If omitted, debate the94 **current branch's** working-tree diff.95- **`--base <branch>`**: ref to diff against. Always a **remote-tracking ref**, never96 a stale local branch. Default: `origin/<PR base>` when a PR number is given, else97 the repo default branch as `git symbolic-ref --short refs/remotes/origin/HEAD`98 (e.g. `origin/master`) — used **as-is**, NOT stripped to local `master` (which99 can lag the remote). Fallback `origin/master`. Step 1 runs `git fetch origin`100 first so the ref is current. The workflow then resolves this to the **merge-base**101 of `base` and HEAD and diffs against that, so commits `base` gained since the102 branch forked aren't reviewed as part of this change.103- **`--no-commit`**: don't commit per round — leave all agreed changes104 uncommitted in the working tree for you to commit yourself. Default is to105 **commit each round** (see below).106- **`--no-comment`**: don't post the debate summary to the PR. By **default**, when107 a PR exists, the debate summary IS posted as a PR comment (see step 3). Pass108 this to suppress the outward-facing write and report in chat only.109- **`--rationale <note>`** (optional): the author's note on **deliberate** design110 decisions. Threaded into **both** sides — codex's round-1 review prompt (so it111 doesn't flag intentional choices as defects; its warm session carries the note112 across later rounds) and the Claude author's prompt **every round** (so it113 *disputes*, rather than "fixes", a finding that contradicts a deliberate choice).114 Mirrors `/lens-debate`'s `rationale`. Pull it from the PR/issue description, or the115 caller (`/be-review`) passes the change rationale straight through.116- **`--context <note>`** (optional): the **main-agent context** the Claude author117 should **inherit** — what this change is FOR (its task/intent and key decisions the118 orchestrator already holds). Injected into the author's prompt **every round** so it119 no longer reconstructs intent from the diff alone (`agent()` is one-shot and can't120 be resumed the way codex is, so re-injection is how it inherits at all). Given to121 the **author only**, not codex — codex stays an independent reviewer of the code,122 not the author's narrative. `/be-review` passes the task context through.123124## Steps125126### 1. Resolve context127128- Determine `repoPath` (the worktree root, normally the cwd).129- **`git fetch origin`** so remote-tracking refs are current — the base is an130 `origin/...` ref, and a stale one would diff against the wrong tree.131- Resolve `base` per the rules above (a remote-tracking ref like `origin/master`).132- If a PR number was given, `gh pr checkout <n>` and confirm the branch.133- Confirm there is a non-empty diff: `git diff --stat <base>`. If empty, tell the134 user there's nothing to review and stop.135- **Preflight codex**: `codex login status`. If not logged in, stop and tell the136 user to run `codex login` (suggest the `!` prefix to do it in-session).137138### 2. Run the debate Workflow139140Invoke the **`Workflow` tool** pointing at this skill's committed script, passing141context through `args`:142143```144Workflow({145 scriptPath: ".claude/skills/codex-debate/debate.workflow.js",146 args: {147 repoPath: "<worktree root>", // also the per-worktree scratch dir root148 base: "<base branch>",149 commit: <false only if --no-commit>,150 skillDir: ".claude/skills/codex-debate",151 context: "<main-agent context the author inherits; omit/'' if none>",152 rationale: "<author's note on deliberate decisions; omit/'' if none>"153 }154})155```156157The workflow runs in the background and notifies you when it completes. It158alternates `codex:roundN` and `claude:roundN` agents under a **Debate** phase —159the user can watch live via `/workflows`. Each Claude round edits the working160tree and (unless `--no-commit`) **commits exactly that round's changed files in161the same session** — one commit per round, with a message embedding the round's162codex findings and Claude's dispositions — never pushing or merging. (The commit163is no longer a separate `commit:roundN` agent: the author already has the tree164open, so it commits its own round.)165166Ephemeral scratch (verdicts, the debate ledger) lives under the gitignored,167per-worktree `<repoPath>/.codex-debate/`, so **parallel debates in different168worktrees never collide** and the scratch never shows up in the diff codex169reviews. It returns:170171```172{ status: "consensus" | "commit-incomplete" | "section-incomplete" | "reviewer-error",173 rounds, base, finalVerdict, filesChanged, commitGaps, sectionGaps, transcript,174 commentHeader, // the comment's small deterministic header (badge + round count + effort + base)175 workDir, sectionGlob } // where the per-round section files live; cat them under the header (step 3)176```177178(each `transcript[]` round also carries a `commit` SHA when that round committed;179`commitGaps` lists the round numbers whose author edited files but returned no180commit SHA — empty unless `status === "commit-incomplete"`; `sectionGaps` lists the181round numbers whose author left no disposition section file — empty unless182`status === "section-incomplete"`.)183184There is one more, **earlier** terminus the workflow can return **before** the185debate loop even starts — `merge-base-error`. If `git merge-base <base> HEAD`186fails (a missing/typoed/stale base, or unrelated history), the diff scope can't be187trusted, so the workflow **aborts up front** rather than review the base branch's188drift as if this change made it. It returns a **different, smaller shape** — no189`commentHeader`, `workDir`, or `sectionGlob`, because no debate (and so no section190files) ever ran:191192```193{ status: "merge-base-error", base, rounds: 0, transcript: [], finalVerdict: null,194 note } // human-readable: which base failed and how to fix it (e.g. `git fetch`)195```196The debate is recorded as small Markdown section files under `<workDir>` — **two197per round**: `section-NNN-1-codex.md` (codex's verdict + findings) and198`section-NNN-2-claude.md` (the author's per-finding dispositions), zero-padded so199the `section-*.md` glob sorts in chronological order. **Each file is written by the200party that owns its content**: a Haiku writer renders codex's small *structured*201verdict to disk, and the **author writes its own dispositions directly** — the same202way codex writes its verdict to a path. That author-writes-its-own-file design is203deliberate: it keeps the author's *structured* return a **minimal ack**204(`filesChanged`/`commitSha`/`done`), so a big multi-finding narrative can never205overflow the structured-output encoding (the failure that used to crash the debate206on large diffs). The author's disposition file does triple duty — it's the author's207cross-round **memory**, the **rebuttal** codex reads next round (`codex-review.sh`208cats it straight into codex's prompt), and the **published comment** (step 3 cats209the section files under `commentHeader`). So the comment is still a **deterministic**210render — a shell `cat`, never re-improvised through an agent, nothing weak ever211retyping a large blob. codex is *not* a memory reader — it keeps its own warm212session and only ever reads the one rebuttal file.213214- **consensus** — every finding codex raised is resolved (any severity — Claude215 fixed it or codex conceded the dispute). This is the *only* way the debate ends216 *normally*: it keeps running rounds until codex and Claude agree on every point,217 with no round cap and no deadlock exit. (The harness's own218 per-workflow agent backstop is the sole hard ceiling; if you ever need to stop219 a debate by hand, interrupt it via `/workflows` or `TaskStop`.)220- **commit-incomplete** — the debate *converged* (codex approved, nothing open),221 but a round's author edited files yet returned **no commit SHA**, so its222 in-session commit didn't land and the "one commit per round" contract broke for223 the round(s) in `commitGaps`. The edits are **not lost** — they stay in the224 working tree and the next reviewer diffs them against the base — but this is225 **not** a clean consensus: a human must reconcile the uncommitted round(s)226 (e.g. commit the outstanding tree) before relying on the per-round history. Do227 **not** report it as a plain consensus (see step 3).228- **section-incomplete** — the debate *converged*, but a round's author **skipped229 or under-filled its disposition section file** (`section-NNN-2-claude.md` missing,230 empty, or missing a backticked marker for an open finding, for the round(s) in231 `sectionGaps`). That file is the hole-free trail everyone draws on —232 the author's memory, the rebuttal codex reads next round, and part of the posted233 comment — so a miss means the published record has a gap. The code guards against234 feeding an empty rebuttal to codex (it warns and keeps the prior pointer), and the235 tree edits are still present, but this is **not** a clean consensus: a human must236 fill in the missing round(s) before trusting the per-round record. Do **not**237 report it as a plain consensus (see step 3).238- **merge-base-error** — an *up-front* abort, **before** any debate round runs:239 `git merge-base <base> HEAD` failed (missing/typoed/stale base, or unrelated240 history), so the review scope can't be trusted. The return carries a human-readable241 `note` (which base failed, how to fix it — e.g. `git fetch`) and **none** of the242 comment-assembly fields (`commentHeader`/`workDir`/`sectionGlob`), because no243 section files were ever written. Report the scope failure and **skip comment244 assembly/posting entirely** (see step 3); fix the base ref and re-run.245- **reviewer-error** — the one *abnormal* terminus: codex itself failed to246 produce a verdict (broken/unavailable CLI), so the workflow synthesized an247 error verdict and aborted rather than spin forever on a dead reviewer. This is248 **infrastructure failure, not a debate outcome** — `finalVerdict.summary`249 carries the failure detail (including how many attempts were made). Do **not**250 treat it as consensus (see step 3). **Transient failures are retried first:**251 `codex-review.sh` retries the `codex exec` invocation with linear backoff252 (default 3 attempts; tune via `CODEX_REVIEW_RETRIES` / `CODEX_REVIEW_BACKOFF`)253 and only synthesizes the reviewer-error verdict once every attempt comes back254 empty — so a single codex hiccup no longer sinks the round.255256### 3. Present the result257258**First branch on `status`.** If `status === "merge-base-error"`, the workflow259**aborted before any debate ran** — `git merge-base <base> HEAD` failed, so the260review scope couldn't be trusted. This return has **no** `commentHeader`,261`workDir`, or `sectionGlob` (no section files exist), so there is **nothing to262assemble**: do **not** run the posting block below. Report the scope failure —263surface the return's `note` (it names the failing base and the fix) — tell the264user to repair the base ref (e.g. `git fetch`, fix a typo'd/stale ref) and re-run,265and **skip the rest of this section**.266267If `status === "reviewer-error"`, the debate did268**not** reach consensus — codex never produced a real verdict. Report it as a269**failure**, not a success: surface `finalVerdict.summary` (and the workflow log)270so the user sees codex was broken/unavailable, and tell them to fix codex (e.g.271`codex login`, check the CLI) and re-run. Do **not** post a consensus badge or a272`## Codex ⇄ Claude debate` PR comment for this path — there is no agreement to273report. Skip the rest of this section.274275If `status === "commit-incomplete"`, the debate converged but at least one round276left its edits **uncommitted** (the round numbers are in `commitGaps`). Report it277as **converged-but-not-clean**: assemble and post the comment (see the posting278block below — `commentHeader` already shows a `⚠️` badge, not the consensus279check), then tell the user which round(s) are uncommitted and that the outstanding280tree must be committed before the per-round history can be trusted. Do **not** call281it a clean consensus.282283If `status === "section-incomplete"`, the debate converged but at least one round's284author **skipped or under-filled its disposition section file** (missing, empty, or285omitting a marker for an open finding; round numbers in `sectionGaps`).286Report it as **converged-but-not-clean**: assemble and post the comment as usual287(the `⚠️` badge is already set), then tell the user which round(s) are missing288their disposition record and that the per-round history has a gap a human should289fill before trusting it. Do **not** call it a clean consensus.290291Otherwise (`status === "consensus"`) report in chat (do **not** push or merge —292the per-round commits sit on the local branch for the human to review):293294- The outcome — **consensus** — and how many rounds it took to get there.295- **The reviewer's reasoning effort** — sourced from the workflow's single296 `REASONING_EFFORT` constant (`xhigh` today), which is passed down to297 `codex-review.sh`'s `-c model_reasoning_effort` and into the comment header, so298 the published value and the config codex actually ran at share one home. Read299 it off the header rather than asserting it independently. State it so the depth300 of the review is on the record.301- `git log --oneline <base>..HEAD` (the per-round debate commits) and302 `git diff --stat <base>` so the user sees what the debate changed.303- A compact per-round summary — read it straight from the section files304 (`cat <workDir>/section-*.md`: each round's codex verdict, then the author's305 dispositions and commit SHA) so the convergence reads round by round. No need to306 re-derive it from `transcript`; the sections already render it.307- The agreed changes are committed per round on the local branch (or, under308 `--no-commit`, uncommitted in the working tree). The user reviews, then pushes309 / merges (or runs `/do --from post-implement`) when satisfied.310- **Post the debate summary to the PR (default).** When a PR exists and311 `--no-comment` was NOT passed, **assemble** the comment from the workflow's312 return — the small `commentHeader`, then the per-round section files `cat`-ed in313 glob order — and `gh pr comment <pr> -F <file>`:314315 ```bash316 mkdir -p "$workDir" # reviewer-error/--no-commit runs may not have created it317 {318 printf '%s\n' "$commentHeader"319 for f in "$workDir"/section-*.md; do printf '\n'; cat "$f"; printf '\n'; done320 } > "$workDir/comment.md"321 gh pr comment <pr> -F "$workDir/comment.md"322 ```323324 (`$workDir` is the returned `workDir`, i.e. `<repoPath>/.codex-debate`; the325 `for`-loop guarantees a blank line between sections regardless of each file's326 trailing newline.) The result is the `## Codex ⇄ Claude debate` header (consensus327 badge, round count, the **reasoning-effort** note from the workflow's328 `REASONING_EFFORT` constant) followed by the per-round breakdown of codex's329 findings and the author's dispositions — the **same** section files the author330 read as memory and codex read as the rebuttal. So the comment is a331 **deterministic** shell concat of the record everyone drew on, not an332 LLM-improvised table — nothing weak ever retypes a blob. This is an outward-facing333 write — on by default because the whole point is to leave the review trail on the334 PR; `--no-comment` suppresses it.335336<a id="answer-mode"></a>337# Answer mode — Codex ⇄ Claude answer debate338339When the argument is a **freeform prompt** (not a PR number/flags), the skill340generalizes the same debate machinery from *reviewing a diff* to *answering a341question*. The shape is **symmetric**, not author⇄reviewer: **Claude and codex are342two equal peers**. They each answer the prompt **independently and in parallel**,343then **cross-check each other's answer** round after round — conceding where the344other is right, holding firm (with evidence) where it isn't — **until both agree**.345A final pass **synthesizes their two converged answers into one unified reply**,346which you present to the user along with a saved transcript.347348Both peers are **codebase-aware but read-only**: each may read this repo (`git349diff/log`, read files, grep) to ground its answer, but neither edits anything —350codex stays under `--sandbox read-only` (kernel-enforced), and the Claude peer is351instructed not to write. Consensus is **schema-detected in code**: each side emits352a structured answer with an `agreesWithOther` boolean and an `objections` list, and353the loop ends only when **both** sides report no remaining disagreement. There is354**no round cap and no deadlock exit** — same as review mode.355356## Steps357358### A1. Resolve context359360- Determine `repoPath` (the worktree root, normally the cwd).361- Capture the **prompt**: everything **after the `answer` subcommand token** (strip362 surrounding quotes). If it's empty, ask the user what they want answered and stop.363- **Preflight codex**: `codex login status`. If not logged in, stop and tell the364 user to run `codex login` (suggest the `!` prefix to do it in-session).365- No `git fetch` / base resolution / `gh pr checkout` here — answer mode doesn't366 diff a branch.367368### A2. Run the answer Workflow369370Invoke the **`Workflow` tool** pointing at this skill's committed answer script,371passing the prompt through `args`:372373```374Workflow({375 scriptPath: ".claude/skills/codex-debate/answer.workflow.js",376 args: {377 repoPath: "<worktree root>", // also the per-worktree scratch dir root378 prompt: "<the user's freeform prompt, verbatim>",379 skillDir: ".claude/skills/codex-debate"380 }381})382```383384The workflow runs in the background and notifies you when it completes. It runs an385**Answer** phase (round 1: `claude:round1` and `codex:round1` in parallel), a386**Reconcile** phase (rounds 2+: each side cross-checks the other, in parallel,387round after round), and a **Synthesis** phase that merges the two agreed answers.388Watch live via `/workflows`. Ephemeral scratch (per-side answers, cross-check389files, per-round sections, the saved transcript) lives under the gitignored,390per-worktree `<repoPath>/.codex-debate/`, so parallel debates never collide. It391returns:392393```394{ status: "consensus" | "reviewer-error" | "agent-error" | "synthesis-error" | "no-prompt",395 rounds, prompt, finalAnswer, transcriptPath, reasoningEffort, codexError }396```397398- **consensus** — the only normal terminus: both sides agreed and then both399 **approved the synthesized candidate** (see the convergence note), and400 `finalAnswer` is that approved unified answer. `transcriptPath` points at the saved401 Markdown transcript (`.codex-debate/answer-<slug>.md`).402- **reviewer-error** — codex itself failed to produce an answer (broken/unavailable403 CLI) after retries; `codexError` carries the failure detail. Infrastructure404 failure, not a debate outcome.405- **agent-error** — one side died on a terminal API error after retries.406- **synthesis-error** — both sides DID agree, but the final synthesis pass produced407 no answer (the synthesis agent died or returned empty). Not a successful answer —408 report it as a failure (there is agreement on record, only the merge failed).409- **no-prompt** — the prompt was empty (shouldn't happen if A1 guarded it).410411### A3. Present the result412413- If `status === "consensus"`: present **`finalAnswer`** to the user as the answer414 — this is the unified reply both Claude and codex agreed on. State **how many415 rounds** it took to converge and that **codex answered at `reasoningEffort`**416 (read it off the return value). Point the user at the saved transcript417 (`transcriptPath`) for the full convergence trail; optionally `cat` the418 `.codex-debate/answer-section-*.md` files to show a compact per-round summary419 (each side's answer, what changed, remaining objections). This mode makes **no420 outward-facing writes** — no PR comment, no commits — it just answers.421- If `status !== "consensus"`: report it as a **failure**, not an answer. Surface422 `codexError` (for `reviewer-error`) or the workflow log so the user sees what423 broke, and tell them how to fix it (e.g. `codex login`) and re-run. Do **not**424 present a half-debate as if it were an agreed answer.425426## Answer-mode safety & notes427428- **Both peers read-only — but enforced ASYMMETRICALLY.** codex runs under429 `--sandbox read-only` (kernel-enforced, belt-and-suspenders with the prompt text —430 it reads arbitrary repo files and could be prompt-injected). The **Claude peer is431 only prompt-enforced**: the harness's `agent()` exposes no sandbox/tool restriction432 (the same is true of every Claude reviewer in `/lens-debate` and review mode), so433 Claude's read-only behaviour rests on instruction, not a kernel guard. A434 prompt-injected or mistaken Claude agent *could* in principle edit files or run a435 git write — answer mode does not, and cannot here, harden against that the way it436 does for codex. If that risk matters for a given prompt, run the debate in a437 disposable/read-only worktree. Treat the read-only guarantee as **hard for codex,438 best-effort for Claude.**439- **Warm codex session.** Round 1 cold-starts `codex exec`; every later round440 resumes the same session (`codex exec resume`) so codex cross-checks from its own441 prior answer rather than reconstructing it. The session id lives in the442 gitignored per-worktree `.codex-debate/` (a distinct `codex-answer-session.id`,443 so it never collides with review mode's session), degrading gracefully to a cold444 start if capture ever fails.445- **Symmetric convergence, schema-detected, candidate-confirmed.** Each side emits446 `agreesWithOther` + `objections`; a side counts as agreeing only when it sets447 `agreesWithOther:true` AND leaves no objection, so a stray objection can't be448 papered over by an over-eager boolean. Because the two run in parallel each round,449 a single mutually-agreeing round can be a **swap false positive** (Claude adopts450 codex's prior answer while codex adopts Claude's — both report agreement, but their451 current outputs are swapped and still differ), and they can keep swapping back and452 forth, so counting consecutive parallel agreements does **not** prove the current453 outputs match. The only sound test is to make both sides judge **one shared piece454 of text**. So when a round shows mutual agreement, the workflow synthesizes a single455 **candidate** from the two agreed answers and runs a **confirmation phase**: both456 sides review that *identical* candidate (without rewriting their own answer) and457 either approve it or object. Approval is on one fixed text both actually saw, so no458 swap is possible; if both approve, that candidate is the converged answer — already459 signed off by both debaters (which is also why `finalAnswer` is never unapproved460 synthesized text). If either objects, the candidate is dropped and the cross-check461 loop resumes with the objections folded in. No round cap, no deadlock exit.462- **Chat + saved transcript, no outward writes.** The unified answer is presented463 in chat and the full transcript is saved to the gitignored464 `.codex-debate/answer-<slug>.md`. Unlike review mode, answer mode never commits465 or posts to a PR.466467## Safety & notes (review mode)468469- **codex runs read-only — enforced, not just asked.** codex is invoked with470 `--sandbox read-only`, so the kernel sandbox blocks file writes and other471 state-mutating syscalls; the prompt's "don't write" instruction is belt-and-472 suspenders, not the only guard. This matters because codex reviews arbitrary473 diffs and could be prompt-injected by file contents. The only writes to the474 tree come from the Claude author rounds. (codex auto-falls-back to its bundled475 bubblewrap when the system one is absent, so read-only works in containers.)476 Resume rounds enforce the same read-only policy via `-c sandbox_mode=read-only`477 (the `resume` subcommand has no `--sandbox` flag) — same kernel guard, set478 through config instead of the flag.479- **Warm reviewer session.** Round 1 cold-starts `codex exec`; the runner records480 codex's session id (its `thread_id`, captured from the `--json` event stream)481 under the scratch dir and every later round `codex exec resume`s it, so codex482 retains its own prior review across rounds. The session id lives in the483 gitignored per-worktree `.codex-debate/`, so parallel debates never resume each484 other's sessions. If the id is ever missing (round-1 capture failed), a later485 round transparently cold-starts with the full prompt + rebuttal — graceful486 degradation, never a wedge.487- **Warm author (context, not session).** The Claude author can't be resumed the488 way codex is — `agent()` is one-shot and Claude isn't headless under Max auth,489 so there's no session id to carry forward. The equivalent is context, not state:490 each follow-up round the author **reads the per-round section files**491 (`cat .codex-debate/section-*.md`) — every prior round's codex findings and its492 own dispositions — so it builds on its last round rather than re-deriving the493 whole diff, and won't re-fix or re-litigate findings already settled. Each round494 writes two small files (a Haiku-rendered codex section and the author's own495 disposition section), so round N>1 always sees rounds 1..N-1; round 1 has none496 yet, so it's byte-identical to a cold start (and if no sections exist, the author497 falls back to the diff + verdict). The *same* sections compose the PR comment step498 3 posts and the rebuttal codex reads, so the author's memory, the published499 summary, and codex's rebuttal are one record. Crucially, the author **writes its500 own disposition section directly** — it never has to pour that narrative through a501 structured field — so its structured return stays a minimal ack and can't overflow502 on a large, many-finding diff (the failure this design replaced). The Haiku writer503 only ever renders codex's small *structured* verdict, so nothing weak retypes a504 large blob. codex stays on its own warm session and never reads the sections —505 only the one rebuttal file each round.506- **Inherited context, not just diff.** On top of that cross-round memory, when the507 caller passes `context` and/or `rationale` the author **inherits them in EVERY508 round's prompt** — the main-agent intent (what the change is FOR) and the509 deliberate-decision note. So even **round 1** reasons from the change's purpose510 rather than the diff alone, and the author *disputes* a finding that contradicts a511 deliberate choice instead of dutifully "fixing" it. The `rationale` also rides512 codex's round-1 prompt (see `codex-review.sh`), so the reviewer doesn't raise those513 intentional choices at the source; `context` is the author's alone (codex stays an514 independent reviewer of the code, not the narrative).515- **Commits, but never pushes or merges.** Each round is committed locally (unless516 `--no-commit`) so the PR history reads as the debate, but the skill never517 pushes or merges. Consensus means "both AIs agree on the committed code," not518 "ship it" — the human reviews the commits and pushes/merges.519- **Parallel-safe.** Ephemeral scratch (verdicts and the per-round section files,520 the author-written ones doubling as the rebuttal) lives under the gitignored,521 per-worktree `<repoPath>/.codex-debate/`, so debates on many worktrees run at once522 without clobbering each other — no shared `/tmp` paths, and each worktree's section523 files are its own.524- **Posts to the PR by default.** When a PR exists, the debate summary — the525 `commentHeader` followed by the per-round section files `cat`-ed together (step 3)526 — is posted as a PR comment (outward-facing write) unless `--no-comment` is passed527 — the point is to leave the review trail on the PR.528- **Runs to consensus — no cap, no deadlock exit.** The loop ends only when codex529 and Claude agree; it does not bail out at a round cap or declare a "deadlock," because530 a debate that quits without agreement is pointless. The two sides keep arguing531 until one concedes. The harness's own per-workflow agent backstop is the sole532 hard ceiling; interrupt via `/workflows` or `TaskStop` if you ever need to stop533 one by hand. **The one carve-out is *not* a deadlock exit:** a finding that is *not a534 code edit for this worktree* but a downstream / ship-phase / process gate (a companion535 repo pinning this repo's final post-review HEAD, a CI/release step, a cross-repo PR)536 cannot be satisfied during the review — it targets the *post*-gauntlet HEAD. When537 CLAUDE shows a finding is such a gate, codex marks it **resolved-and-deferred**538 (acknowledged, handed to the ship phase) instead of holding it open forever. The CODE539 debate still converges to consensus the normal way; this only stops the loop spinning540 on a process gate neither side can land mid-review. It is narrow by design — a genuine541 code defect CLAUDE simply dislikes is still argued to consensus, no exit. (This is the542 loop that once spun until a human killed it on a `@kolu/surface` cross-repo run.)543544## Files545546Shared:547548- `scripts/codex-exec-lib.sh` — the sourced core both modes share: the read-only549 `codex exec`/`resume` invocation, warm-session resolve/persist, retry/backoff,550 thread-id capture, and the synthesized error-verdict fallback (via a caller hook).551 The two mode scripts source this and add only their own prompts + verdict shape.552553Review mode:554555- `debate.workflow.js` — the Workflow script (the loop + consensus logic).556- `scripts/codex-review.sh` — the review-specific invocation (arg parsing, the557 review prompts, the verdict schema/session file, the verdict-shaped error).558- `scripts/codex-verdict.schema.json` — the JSON Schema codex's verdict is constrained to.559560Answer mode:561562- `answer.workflow.js` — the Workflow script for the symmetric answer-debate563 (parallel answers → cross-check loop to agreement → synthesis).564- `scripts/codex-answer.sh` — the answer-specific invocation (arg parsing, the565 answer prompts, the answer schema/session file, the answer-shaped error).566- `scripts/codex-answer.schema.json` — the JSON Schema codex's answer is constrained to.567568These are generated from `agents/.apm/skills/codex-debate/`; edit the source there and569run `just ai apm` to regenerate.570571ARGUMENTS: $ARGUMENTS