Multi-Agent Debate CLI for Actionable Decision Support — Turn structured AI debate into action plans, trade-offs, and risks.
中文 | English
Crossfire is a terminal-first multi-agent debate orchestrator for decision support. It runs structured proposer vs. challenger debates across Claude, Codex, and Gemini, then synthesizes the result into a prioritized action plan in Markdown and HTML.
Use it to stress-test architecture proposals, migration plans, product bets, and incident responses. The debate is the mechanism; the main deliverable is the final action plan, backed by transcripts and replayable event logs.
- Highlights
- Best For
- What You Get
- Quick Start
- Execution Modes
- TUI
- CLI Reference
- Runtime Commands
- Supported Agents
- Profiles
- Output Files
- How It Works
- System Model
- Architecture Overview
- Current Limitations
- Extending Crossfire
- Contributing
- Action-plan first — The primary output is
action-plan.html/action-plan.md, not just a debate transcript - Multi-provider — Mix and match Claude (Agent SDK), Codex (JSON-RPC), and Gemini (subprocess) in any role
- Real-time TUI — Split-panel terminal UI with live streaming, retained thinking summaries, persistent pre-tool narration blocks, wrapped tool-call traces, locally tracked live tool elapsed timers, highlighted approval cards, and convergence metrics
- Event sourcing — Every event is persisted to JSONL. Resume interrupted debates and replay completed ones from the same source of truth
- Structured extraction — Agents report stance, confidence, key points, and concessions via tool calls (Zod-validated)
- Judge arbitration — Optional judge agent scores arguments, detects stagnation, and emphasizes evidence responsibility instead of rewarding unsupported claims
- Adaptive final synthesis — After the debate, Crossfire generates a final action plan in a fresh synthesis session, forces synthesis into a tool-free planning turn, and falls back to a structured local report if model-backed synthesis fails
- Incremental prompts — Turn 1 sends full context; Turn 2+ sends only new opponent/judge messages, leveraging provider session memory for ~O(1) per-turn cost
- Config-driven setup —
crossfire.jsondefines roles, provider bindings, MCP servers, and policy presets in one file - Policy presets — Set debate defaults, per-role baselines, and per-turn overrides such as
research,guarded,dangerous, andplan
- Architecture review — Pressure-test design proposals, trade-offs, and migration plans
- Product decisions — Surface hidden assumptions before committing to a roadmap or bet
- Risk discovery — Force explicit concessions, counterarguments, and unresolved concerns
- Research synthesis — Turn competing perspectives into a structured action plan in one terminal workflow
- Live debate view — Full-screen terminal UI for round-by-round reasoning, retained thinking summaries, persistent pre-tool narration, wrapped tool details, locally tracked live tool elapsed timers, approval prompts, judge feedback, and convergence tracking
- Action plan outputs — Final report in Markdown and HTML for sharing, editing, or automation
- Full transcript — Human-readable transcript in Markdown and HTML
- Replayable audit trail — Event-sourced JSONL logs plus
index.jsonmetadata for replay, resume, and status inspection
git clone https://github.com/jyzhan/crossfire.git
cd crossfire
pnpm install
pnpm buildOption A: Global command (recommended)
pnpm setup # ensures PNPM_HOME is in PATH (restart terminal after)
pnpm -C packages/cli link --global # makes `crossfire` available globally
crossfire --version # verifyIf
pnpm setupreports "already up to date" butcrossfireis not found, add pnpm's global bin to your PATH manually:echo 'export PNPM_HOME="$HOME/Library/pnpm"' >> ~/.zshrc # macOS echo 'export PATH="$PNPM_HOME:$PATH"' >> ~/.zshrc source ~/.zshrc
Option B: Run directly (no global install needed)
node packages/cli/dist/index.js <command> [options]Before your first run, make sure the agent CLI used by your selected profiles is installed, authenticated, and works in your shell.
# First, create a config file (crossfire.json) with roles and provider bindings
# See the "Config File Format" section below for details
# Claude vs Claude (judge auto-inferred)
crossfire start \
--config crossfire.json \
--topic "Should we adopt microservices?" \
--max-rounds 5 \
--output run_output/microservices
# With explicit presets
crossfire start \
--config crossfire.json \
--topic "Is caching always better than recomputing?" \
--proposer-preset research \
--challenger-preset guarded
# Headless mode (no TUI, completion info still printed to stdout)
crossfire start \
--config crossfire.json \
--topic "Quick brainstorm" \
--headless -vIn the example above, output lands in run_output/microservices/ because --output run_output/microservices is set explicitly. If you omit --output, Crossfire writes to a timestamped debate directory such as run_output/d-20260331-224500/. Inspect action-plan.html or action-plan.md there, use crossfire status <output-dir> for a summary, and crossfire replay <output-dir> to replay the event log.
Policy presets are Crossfire's way to reduce approval fatigue without flattening every provider into the same approval protocol.
Think of them as an orchestration-level policy layer:
- Crossfire decides how interactive a turn should be
- each adapter maps that decision to the strongest official primitive the provider actually exposes
- the event log and TUI record which mode was really used for each turn
There are three baseline modes and one special override:
researchLow-interaction research. Prefer safe or read-oriented behavior and reduce approval noise. For Claude, this also applies a conservative per-turn query cap so one research turn does not keep expanding forever.guardedNormal controlled execution. This is the closest thing to the old default behavior.dangerousHigh-trust execution. Minimize approval interruptions and accept higher risk.planPer-turn special mode. Use it when you want an agent to outline what it intends to do before giving it a more permissive execution mode.
Why plan is not a normal baseline:
- debate quality usually depends on real reads, searches, and verification
- a permanent
planbaseline would turn many turns into pure LLM reasoning with weaker evidence - in practice,
planis most useful as a one-turn preview, not as a steady-state runtime mode
Preset precedence is:
CLI role-specific > CLI global > config file > role default
That means:
--presetsets the debate-wide default for all roles--proposer-presetand--challenger-presetoverride the default for one role--turn-preset p-1=planor--turn-preset c-2=dangerouswins for that one turn only- Config file can set per-role
presetfields as baseline defaults
Examples:
# Debate-wide default
crossfire start \
--config crossfire.json \
--topic "Should we migrate to Rust?" \
--preset guarded# Different baselines by role
crossfire start \
--config crossfire.json \
--topic "Should we rebuild the auth service?" \
--proposer-preset research \
--challenger-preset guarded# Force a one-turn planning preview before proposer round 1
crossfire start \
--config crossfire.json \
--topic "Should we move to event sourcing?" \
--proposer-preset research \
--turn-preset p-1=planCurrent provider mapping is intentionally asymmetric:
- Claude
research -> dontAsk + allowlist + bounded maxTurns,guarded -> default,dangerous -> bypassPermissions,plan -> plan - Codex maps to approval and sandbox policy combinations rather than a single mode field
- Gemini currently only gets startup / per-turn approval-profile mapping in headless mode; do not assume parity with Claude or Codex
Practical guidance:
- start with
--proposer-preset researchwhen the proposer tends to do broad evidence gathering - for Claude,
researchis intentionally bounded; if you need a longer free-form exploration turn, step up toguardedordangerous - keep
--challenger-preset guardedwhen you still want explicit control over stronger validation actions - use
--turn-preset p-1=planwhen you want to inspect the agent's intended workflow before letting it execute - use
dangerousonly for trusted, well-bounded tasks where interruption cost matters more than reviewability
The terminal UI is a full-screen Ink (React for CLI) application with four stacked regions:
- Header bar — Centered branding, debate ID, round/phase, proposer & challenger agent info, and topic
- Scrollable content — Round-by-round display of agent messages, thinking traces, and wrapped tool calls so long commands and inputs stay readable. Scroll with arrow keys,
Ctrl+U/Ctrl+D, orHome/End - Tool liveness — Running tool rows and live headers show a locally maintained elapsed timer even when the provider only emits
tool.call/tool.resultwithout intermediatetool.progressevents - Metrics bar — Per-agent token counts and costs, convergence progress bar with percentage, judge verdict, and scroll status (LIVE / SCROLLED). Usage accounting is provider-aware, and token rows now label whether a provider reports
session delta,per turn, orthread cumulativeusage - Fixed live status — The metrics bar reserves its first line for a compact
Active: ...summary of the currently running role, so long tool bursts stay visible even after the round header scrolls out of the live viewport - Compressed tool failures — Live tool views collapse repeated fetch failures into a short summary such as
recent failures: 404×5, 403×2; userun_output/<debate-id>/events.jsonlfor the full raw trace - Live tool focus — The live panel only keeps currently running tools on screen. Successful tools disappear from the live list as soon as they finish, while failures collapse into a short summary; completed details remain available in round snapshots and
events.jsonl - Six-state Claude tool model — Claude tool requests are projected as
requested,running,succeeded,failed,denied, orunknownrather than assuming everytool.callis already executing. A providertool.progressupgrades a request intorunning, permission denials collapse intodenied, and only observed terminal hooks becomesucceeded/failed - Unresolved tool closure — If a turn ends while some provider tool requests never produce a terminal hook, Crossfire closes those live rows as
unknown outcomeinstead of letting them sit inrunningforever - Command/approval area — Context-aware live prompt (
>,approval>) plus expanded approval cards that show the pending tool/command, batch actions, and provider-aware shortcuts such as/approve 2,/approve 2 2, or/approve all - Mode visibility — Proposer and challenger headers append the effective turn mode inline as
Role [provider] [mode: ...], so you can tell at a glance whether a role is inresearch,guarded,dangerous, orplanwithout hunting for a separate status line
Use --headless to skip the TUI. Events and synthesis outputs are still persisted for later inspection.
Start a new debate.
| Option | Description | Default |
|---|---|---|
--config <path> |
Config file with roles and provider bindings | required |
--topic <text> |
Debate topic | — |
--topic-file <path> |
Read topic from file (mutually exclusive with --topic) |
— |
--max-rounds <n> |
Maximum debate rounds before forced termination | 10 |
--judge-every-n-rounds <n> |
Judge intervenes every N rounds (must be < max-rounds) | 3 |
--convergence-threshold <n> |
Stance distance (0-1) below which debate auto-converges | 0.3 |
--model <model> |
Model override for all roles | — |
--proposer-model <model> |
Model override for proposer | — |
--challenger-model <model> |
Model override for challenger | — |
--judge-model <model> |
Model override for judge | — |
--preset <preset> |
Debate default policy preset (research, guarded, dangerous) |
— |
--proposer-preset <preset> |
Proposer baseline policy preset | — |
--challenger-preset <preset> |
Challenger baseline policy preset | — |
--judge-preset <preset> |
Judge baseline policy preset | — |
--turn-preset <turnId=preset> |
Repeatable per-turn override; supports plan |
— |
--template <family> |
Prompt template family for all roles (auto, general, code) |
auto |
--proposer-template <family> |
Proposer prompt template override | inherited from --template |
--challenger-template <family> |
Challenger prompt template override | inherited from --template |
--judge-template <family> |
Judge prompt template override | inherited from --template |
--output <dir> |
Output directory | run_output/d-YYYYMMDD-HHMMSS |
--headless |
Disable TUI (completion info still printed to stdout) | false |
-v, --verbose |
Verbose logging | false |
Validation rules:
--judge-every-n-roundsmust be less than--max-rounds.--convergence-thresholdmust be between 0 and 1.
Policy preset precedence is:
CLI role-specific > CLI global > config file > role default
Model resolution: --proposer-model > --model > config file model field > provider default.
Resume an interrupted debate. State is reconstructed from persisted events.
| Option | Description | Default |
|---|---|---|
--config <path> |
Override config | from index.json |
--headless |
Disable TUI | false |
Config changes (role, preset, model overrides) are allowed on resume, but you cannot add a judge to a debate that started without one.
Replay a completed debate with time-scaled playback. No agent connections needed.
| Option | Description | Default |
|---|---|---|
--speed <n> |
Playback speed multiplier | 1 |
--from-round <n> |
Start from round | beginning |
Current behavior: replay is currently CLI-driven and non-interactive. It replays the stored event stream, but does not expose the live command parser.
Show debate status summary. Add --json for machine-readable output.
Debate Status
=============
Debate ID: d-20260321-143022
Topic: Should we adopt microservices?
Started: 2026-03-21T14:30:22.000Z
Ended: 2026-03-21T14:32:29.300Z
Duration: 2m 7s
Total Rounds: 8
Total Events: 4523
Termination Reason: convergence
Profiles:
Proposer: claude_proposer (claude_code)
Model: us.anthropic.claude-opus-4-6-v1
Challenger: codex_challenger (codex)
Model: gpt-5.4
Judge: claude_judge (claude_code)
Model: us.anthropic.claude-opus-4-6-v1
Configuration:
Max Rounds: 10
Judge Every N Rounds: 3
Convergence Threshold: 0.3
Inspect policy compilation for a given config file. Shows the resolved preset, policy layers, and provider translation for each role.
crossfire inspect-policy --config crossfire.jsonInspect tool/MCP server wiring for a specific role in a config file. Shows available tools and their sources.
crossfire inspect-tools --config crossfire.json --role proposerDuring a live debate started with crossfire start, type commands in the TUI input bar:
| Command | Effect | Status |
|---|---|---|
/stop |
Stop immediately | ✅ |
/inject proposer <text> |
Add context to proposer's next prompt | ✅ |
/inject challenger <text> |
Add context to challenger's next prompt | ✅ |
/inject both <text> |
Add context to both agents' next prompts | ✅ |
/inject! <target> <text> |
High-priority injection for proposer, challenger, or both |
✅ |
/inject judge <text> |
Queue an out-of-band judge turn after the current round with user instruction | ✅ |
/interrupt [role] |
Interrupt the active provider turn when supported | ✅ |
/pause |
Pause the debate after the current turn | ✅ |
/resume |
Resume a paused live debate | ✅ |
/extend <n> |
Increase max rounds by N | ✅ |
Approval mode (auto-activates on approval requests): /approve, /deny, /approve <index>, /deny <index>, /approve <index> <option>, /deny <index> <option>, /approve all, /deny all ✅
The TUI expands pending approvals into a dedicated highlighted block with the provider, approval type, request summary, per-row index shortcuts, provider-aware option rows, and batch approve / reject commands. When every pending approval shares the same session-level allow option, the block now surfaces a direct shortcut such as /approve all 2. Claude tool approvals always expose a session-level allow option through Crossfire's normalized approval capabilities, while Codex approvals can surface native decision variants when the provider sends them.
crossfire resume now reuses the same live command wiring as crossfire start, so /stop, /interrupt, approval commands, inject commands, /pause, /resume, and /extend remain available while resuming an interrupted debate.
Inject semantics:
- proposer / challenger / both injects are one-shot guidance consumed when the next targeted prompt is built
- if you inject the same target multiple times before its next turn, the latest inject replaces the earlier pending one
/inject judgedoes not interrupt the current speaker mid-turn; it queues an extra judge turn at the next post-round checkpoint
Profile agent field |
CLI | Transport |
|---|---|---|
claude_code |
claude |
In-process Agent SDK ≥0.1.77 (async generator + hooks) |
codex |
codex |
Bidirectional JSON-RPC 2.0 over stdio |
gemini_cli |
gemini |
Subprocess per turn |
Any agent can play any role (proposer, challenger, or judge). Mix and match freely.
Crossfire uses a crossfire.json config file to define roles, provider bindings, MCP servers, and policy presets.
Example crossfire.json:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"]
}
},
"providerBindings": [
{
"name": "claude-main",
"adapter": "claude",
"model": "claude-sonnet",
"mcpServers": ["github"]
},
{
"name": "codex-main",
"adapter": "codex",
"model": "gpt-5.4"
}
],
"roles": {
"proposer": {
"binding": "claude-main",
"preset": "research"
},
"challenger": {
"binding": "codex-main",
"preset": "guarded"
},
"judge": {
"binding": "claude-main",
"preset": "plan"
}
}
}| Field | Description | Required |
|---|---|---|
mcpServers |
Shared MCP server definition registry | no |
providerBindings |
Provider binding list | yes |
roles.proposer |
Proposer role config | yes |
roles.challenger |
Challenger role config | yes |
roles.judge |
Judge role config (optional) | no |
Role config fields:
| Field | Description | Required | Default |
|---|---|---|---|
binding |
Provider binding name | yes | — |
model |
Model override for this role | no | binding default |
preset |
Policy preset (research, guarded, dangerous, plan) |
no | role default (guarded for proposer/challenger, plan for judge) |
systemPrompt |
Role-specific system prompt override | no | binding / built-in default |
Provider binding fields:
| Field | Description | Required | Default |
|---|---|---|---|
name |
Binding identifier referenced by roles | yes | — |
adapter |
Adapter type (claude, codex, gemini) |
yes | — |
model |
Binding-level default model | no | adapter default |
providerOptions |
Provider-native escape hatch (not policy semantics) | no | — |
mcpServers |
Attached MCP server names from the top-level registry | no | [] |
Note: The profile-based approach (
--proposer <profile>,--challenger <profile>) is deprecated in favor of--config <path>. Built-in provider profiles are still used internally for testing and examples, but production usage should migrate to the config file format.
Crossfire previously used separate provider/runtime profiles from reusable role prompts.
Provider profiles are JSON files:
{
"name": "my_debater",
"description": "A skilled technical debater",
"agent": "claude_code",
"model": "us.anthropic.claude-opus-4-6-v1",
"prompt_family": "auto",
"inherit_global_config": true,
"mcp_servers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-filesystem"]
}
}
}| Field | Description | Required | Default |
|---|---|---|---|
name |
Profile identifier | yes | — |
description |
Human-readable description | no | — |
agent |
Agent type (claude_code, codex, gemini_cli) |
yes | — |
model |
Preferred model | no | provider default |
prompt_family |
Default prompt family (auto, general, code) |
no | auto |
inherit_global_config |
Merge user's global MCP config | no | true |
mcp_servers |
Profile-specific MCP servers | no | {} |
Prompt contracts are plain Markdown files under prompts/<family>/<role>.md.
Search paths:
- provider profiles:
./profiles/providers/then~/.config/crossfire/profiles/providers/ - prompt templates:
./prompts/then~/.config/crossfire/prompts/
Judge auto-inference: When --judge is omitted, Crossfire picks the judge profile matching the proposer's adapter type (for example, claude/proposer defaults to claude/judge).
Built-in prompting is now split into two layers:
- provider profiles choose the adapter, default model, and runtime wiring
- prompt templates define the
proposer,challenger, andjudgerole contract - template family
generalis for business, product, and research topics - the built-in proposer templates now force executable plans instead of high-level slogans;
general/proposerpushes positioning, economics, controls, channel protection, and operations, whilecode/proposerpushes implementation detail, migration, tests, security, and rollout - the built-in challenger templates now force multi-dimensional challenge coverage instead of generic objections;
general/challengerpushes trust/conversion, pricing abuse, security/compliance, channel leakage, and execution burden, whilecode/challengerpushes correctness, regression risk, testing, security, operations, and rollout - the built-in judge templates now penalize shallow risk lists and weak convergence calls; they explicitly look for evidence-backed challenges, concrete failure modes, and unresolved high-leverage gaps
- template family
codeis for repository, implementation, and debugging topics --template autoruns one lightweight classifier call first, using the judge profile's provider/model to choose betweengeneralandcode- if the classifier times out or returns invalid JSON, Crossfire falls back to a local heuristic
--proposer-template,--challenger-template, and--judge-templatestill override the family manually and skip the classifier
This split is now symmetric across Claude, Codex, and Gemini. Built-in provider profiles no longer hard-code provider-specific role prompts. Instead, all built-in providers share the same reusable general and code role-template families, while adapter/runtime differences stay in the provider profile layer.
Built-in files:
profiles/
└── providers/
├── claude/ # proposer.json, challenger.json, judge.json
├── codex/ # proposer.json, challenger.json, judge.json
└── gemini/ # proposer.json, challenger.json, judge.json
prompts/
├── general/ # proposer.md, challenger.md, judge.md
└── code/ # proposer.md, challenger.md, judge.md
Typical usage:
# Let Crossfire classify the template family automatically
crossfire start \
--config crossfire.json \
--topic "Should we launch an API resale product?" \
--template auto# Force code-oriented prompting for every role
crossfire start \
--config crossfire.json \
--topic "Should we rewrite the data layer?" \
--template code# Use different template families by role
crossfire start \
--config crossfire.json \
--topic "Should we rewrite the data layer?" \
--proposer-template general \
--challenger-template code# Combine role presets, per-turn override, and template selection
crossfire start \
--config crossfire.json \
--topic "Should we rewrite the data layer?" \
--proposer-preset research \
--challenger-preset guarded \
--turn-preset p-1=plan \
--template codeEach debate produces files in its output directory:
| File | Description |
|---|---|
action-plan.html |
Primary final report in HTML |
action-plan.md |
Same action plan in Markdown |
transcript.html |
Full debate transcript in HTML |
transcript.md |
Same transcript in Markdown |
events.jsonl |
Complete event log (one JSON per line) — source of truth |
index.json |
Metadata, byte offsets, segment manifest, profile info, and debate config |
synthesis-debug.json |
Prompt-assembly metadata plus synthesis runtime diagnostics for inspecting or troubleshooting output |
On resume, a new segment file is created (for example, events-resumed-<ts>.jsonl) and tracked in index.json.
Visible transcript-style outputs automatically strip embedded debate_meta / judge_verdict JSON blocks after extraction. The structured payloads remain preserved in events.jsonl and derived state.
If model-backed synthesis fails, Crossfire still writes a fallback action plan so the run produces a usable report. The fallback report is enriched from the debate summary rather than relying only on sparse draft state, keeps the executive summary as short structured paragraphs, and avoids dumping the full judge essay into the recommendation line.
- Profile loading — CLI reads JSON provider profiles, validates them with Zod, and maps
agentto adapter type. - Prompt template resolution — Crossfire resolves the
generalorcodetemplate family from--template, per-role--*-templateoverrides, profileprompt_family, or the lightweight topic classifier fallback, then loadsprompts/<family>/<role>.md. - Adapter creation — Each role gets an
AgentAdapterthat normalizes provider-specific protocols into a shared event stream. - Event bus — All events flow through
DebateEventBus. The TUI, EventStore (JSONL persistence), and TranscriptWriter all subscribe here. - Turn loop — The orchestrator builds prompts from projected state, sends them to the active agent, waits for
turn.completed, then continues with the other side. State is re-projected from events before every decision. - Structured extraction — Agents call
debate_metato report stance, confidence, key points, and concessions. The judge callsjudge_verdictwith scores and continue/stop recommendations, with prompt guidance that penalizes unsupported claims rather than quietly accepting them. - Incremental prompts — Turn 1 sends the full system prompt, topic, and output schema; subsequent turns send only the opponent's latest response plus optional judge feedback.
- Convergence — Crossfire tracks stance delta, concessions, and whether both sides want to conclude. Debates can terminate early when convergence is high enough.
- Persistence — Events batch-flush to JSONL every 100ms, with sync flush on turn/debate completion. The full log enables deterministic replay and resume.
- Final synthesis — After the debate, Crossfire generates
action-plan.md/action-plan.htmlin a fresh synthesis session, runs that synthesis turn in tool-freeplanmode with a longer timeout budget, records synthesis diagnostics for auditing, and falls back to an enriched local report if model-backed synthesis fails.
For the architecture reference set, start at docs/architecture/overview.md.
Note: The architecture docs are maintained as an entry page plus linked subsystem references. When in doubt, the source code is authoritative.
Crossfire is built around a shared event stream rather than mutable runtime state.
- Event log is authoritative — Replay, resume, transcript generation, and status reporting all derive from persisted events
- All state is projected — Runtime state is rebuilt through
projectState(events[]), which keeps replay and recovery deterministic - Pure core / effectful shell —
-corepackages hold testable logic, while outer packages handle CLI, adapters, file I/O, and rendering
packages/
├── adapter-core/ # Shared event model, AgentAdapter interface, Zod schemas, contract tests
├── adapter-claude/ # Claude Agent SDK adapter (in-process async generator)
├── adapter-codex/ # Codex JSON-RPC 2.0 bidirectional stdio adapter
├── adapter-gemini/ # Gemini subprocess-per-turn adapter (A→B fallback)
├── orchestrator-core/ # Pure logic: state projection, convergence, prompt building, director
├── orchestrator/ # Side effects: debate runner, DebateEventBus, EventStore, TranscriptWriter, synthesis
├── tui/ # Ink (React for CLI) components, TuiStore, EventSource/PlaybackClock
└── cli/ # Commander.js entry, JSON profile system, wiring factories
Layer guide:
- Adapters normalize provider protocols into shared events with explicit capability surfaces. Shared type aliases (
AdapterId,DebateRole,UsageSnapshot) reduce duplication across the event model. - Orchestrator core makes replay-safe decisions from projected state
- Orchestrator runs debates, persists events, writes transcripts, and triggers final synthesis. Key helpers (
getLatestTurnContent,invokeJudge,applyRoundToPlan) keep the runner and plan accumulator focused and testable. - TUI renders the same event stream used by live execution and replay. Data-driven lookup maps replace nested ternaries for status icons, prefixes, and color selection.
crossfire replayis currently non-interactive and does not expose the live command parser/jump turn <turnId>is parsed by the TUI but does not have a live handler yetcrossfire statusstill has limited special-casing for truly in-progress debates- external history injection remains an internal adapter recovery capability; there is no user-facing
--history-fileor live import command yet - TODO: if external history injection becomes a product feature, prefer
start/resume --history-file <json>over a live import command so the imported context stays event-sourced and replay-safe replay --from-roundis not reliable across resumed multi-segment runs today
To add a new provider, implement AgentAdapter in a new package, normalize provider output into the shared event model, run the adapter contract tests, then wire adapter creation in the CLI factory layer and add profiles for the new role set.
For design details, start with docs/architecture/overview.md and then read docs/architecture/adapter-layer.md and docs/architecture/orchestrator.md.
Contributions are welcome! See CONTRIBUTING.md for development setup, tech stack, testing instructions, project conventions, and how to add a new adapter.
MIT
Built with Claude Agent SDK, Codex CLI, Gemini CLI, Ink, and Zod.
