Two peer coding agents in a terminal, coordinated through isolated Git worktrees.
Duo runs two coding agents as peers rather than as a planner and a subordinate worker. Start in one of three workflows and switch at any time without restarting the agents:
- Fast (default). Austin drives the implementation; Tony independently verifies the exact commit.
RUNNING → VERIFY → DONE, with no shared plan and no dual sign-off — the quick path for most tasks. - Goal (
duo --mode goal). The full negotiated workflow: a shared plan, isolated worktrees, cross-review and dual sign-off,PLAN → EXECUTE → REVIEW → INTEGRATE → DONE. - Step (
duo --mode step). Plan an ordered queue first, then run every item under its own Fast or Goal policy. Each step lands on your branch as it is approved, with no human task entry between steps.
In all three modes the agents message each other while both keep working, changes stay in isolated Git worktrees, and the verified result is handed back to the repository you launched Duo from.
The runtime is a Go coordination core plus thin driver plugins and optional agent-side bridges. Duo enforces only what benefits from deterministic coordination — identity, routing, mode and phase transitions, verification, signatures, Git evidence — and leaves the models free to collaborate naturally.
cd /path/to/git/repo
duo # FAST (default), gotui dashboard
duo --mode goal # full negotiated workflow
duo --mode step # one complex task, landed step by stepThen type a task in the composer and press Enter. In Fast, Duo sends it to Austin. In Goal and Step, Duo sends it to both Austin and Tony so they can co-design the shared Plan.
- In Fast, Austin works in its own worktree, commits, and calls
duo_set_statusto request verification; Tony verifies that exact commit withduo_set_verificationand either passes it or returns a concrete issue. Duo then delivers the verified commit and marksDONE. - In Goal, Austin and Tony receive the task together, agree a shared Plan with
duo_sendandduo_set_plan, and Duo Core drivesPLAN → EXECUTE → REVIEW → INTEGRATE → DONEfrom there. - In Step, both agents first agree on an ordered queue with
duo_set_stepsand a prose design withduo_set_plan, tagging each item[fast]or[goal]. A Fast item is Austin implementing and Tony verifying; a Goal item is both executing, cross-reviewing and signing. Duo lands each step on your branch and advances the queue by itself.
Requirements:
- Git
- macOS or Linux — Duo owns both agent PTYs directly, so no
scriptwrapper is needed - At least one supported agent CLI on
PATH: Pi, agy, or opencode - Optional:
sqlite3onPATHforagytoken metrics in the work preview
Released binaries (macOS/Linux, amd64/arm64; installs duo, driver helpers and agent bridges):
curl -fsSL https://raw.githubusercontent.com/atfa/duo/main/scripts/install-release.sh | bashFrom source (runs the Go test suite, then installs Duo and its driver bridges; requires Go 1.24+):
./scripts/install.shRestart any running Pi or opencode processes after installing the bridges.
A session launches in Fast (default), Goal or Step mode, and can switch to another mode at any time with /mode fast|goal|step (or /escalate, which is shorthand for Goal). A running round cannot be half one workflow and half another: a switch resets the current plan, queue, phase and signatures and starts the new mode's first phase, while commits already in the worktrees are preserved. Switching in DONE changes the mode for the next task without reopening the completed round. In gotui, a slash-command mode switch (/mode or /escalate) asks whether Austin and Tony should resume immediately; the safe default is Not now, so changing mode before writing the next prompt does not wake either agent. All modes share the same isolated worktrees, Git evidence and delivery handoff; they differ in how much negotiation is required before delivery.
RUNNING → VERIFY → DONE
| Phase | What happens | Gate to advance |
|---|---|---|
| RUNNING | Austin implements and commits in its own worktree. Tony is read-only and may be asked for advice via duo_send. |
Austin calls duo_set_status ready=true with a clean worktree. |
| VERIFY | Tony independently inspects Austin's exact commit. | duo_set_verification with passed (→ delivery → DONE) or issue_found plus a concrete note (→ RUNNING). |
| DONE | The verified commit was delivered to your repository. | Delivery succeeded. |
Fast has no shared Plan and no dual sign-off. A passed verification is bound to the exact commit that was requested; any new commit invalidates it and returns the session to RUNNING. Only Austin writes to the delivered artifact — Tony's worktree is never delivered.
Switching modes: Either the human (/mode goal, /escalate [reason], or /mode step) or an agent (duo_escalate) can switch at any time without restarting either agent. In gotui, /mode and /escalate change the mode first and then ask Resume work? — choosing Not now leaves both agents idle until the human submits the next task. The harness watchdog is held as part of Not now, so it cannot nudge an agent into starting on its own. A Fast → Goal switch while work is active keeps its historical escalation behavior: the session transitions to PLAN with a shared plan seeded from the escalation reason, Tony becomes an active co-developer, and all prior commits are preserved. Other switches start a fresh round under the new mode's gates.
PLAN → EXECUTE → REVIEW → INTEGRATE → DONE
| Phase | What happens | Evidence required to advance |
|---|---|---|
| PLAN | Agents negotiate one shared Plan. Provisional edits are allowed. | Both approve the same Plan version. |
| EXECUTE | Each works independently in their own Git worktree. | Each worktree is clean, with a recorded commit SHA. |
| REVIEW | Each reviews the peer's exact commit. | Each approves the exact peer HEAD reviewed. |
| INTEGRATE | Tony's branch is merged into Austin's integration worktree. | Both approve the same clean integrated Austin HEAD. |
| DONE | The approved artifact has been delivered back to your repository. | Delivery succeeded. |
For one complex task that is worth landing in increments. The queue is the contract:
PLAN → (per step: EXECUTE → REVIEW → INTEGRATE → land the step) → DONE
| Phase | Fast-policy step | Goal-policy step | Evidence required to advance |
|---|---|---|---|
| PLAN | Both agents co-design the ordered queue and prose plan. | Same. | Both approve the same non-empty queue version. Tag each item [fast] or [goal]; untagged means Fast. |
| EXECUTE | Austin implements exactly the current step. Tony is read-only. | Both agents are co-authors and implement in their own worktrees. | Fast: Austin signs duo_set_status ready=true with a clean HEAD. Goal: both agents sign. |
| REVIEW | Tony verifies Austin's exact HEAD. | Both agents cross-review the peer's exact HEAD and sign REVIEW. |
Fast: duo_set_verification passed lands it, issue_found returns it to EXECUTE. Goal: both signatures. |
| INTEGRATE | Duo lands the verified HEAD and advances the queue. | Duo merges Tony's branch into Austin's; both review and sign the integrated HEAD. Non-final: land on both signatures. | Fast: delivery succeeded. Goal: both signatures on the same clean integrated HEAD. |
| DONE | Every step has landed. | Same. | The final step always needs both agents' signatures, whichever policy it uses. |
Each step must be a committable increment whose test command passes at its own HEAD. Duo owns the queue and its cursor — agents never tick a box, and a landed step is never removed or reordered; a step that turns out to be too big is split by appending to the end of the queue.
A Fast step keeps the lighter single-verifier gate: Tony's independent check of one exact HEAD is what makes it safe, and asking Austin to co-sign his own INTEGRATE would be self-approval. A Goal step uses the full heavy chain — independent execution, cross-review, merge and dual integration sign-off — because that is the policy the two agents chose for it in PLAN. The final step always keeps the dual sign-off, so the finished queue is accepted by both agents before delivery. Because each landed step goes to the branch you launched Duo from, the verifier's worktree is fast-forwarded to the landed head after every Fast step, so Tony always reviews and builds the tree you actually received rather than a stale one.
In all modes a phase is a checkpoint, not a behavioral cage: Duo does not stop an agent from reading code or reviewing a diff early; the phase only decides what evidence is required to advance.
DONE is terminal for the round, not for the session. Submitting another task through the Duo composer reopens it for a new round — Fast returns to RUNNING, Goal to PLAN, Step to PLAN with a fresh queue — and clears the finished round's delivery checkpoint so the follow-up can be verified and delivered on its own. Attaching to an agent's own session (Ctrl+A / Ctrl+T) does not route a task through Duo, but it stays recoverable in Fast mode: when Austin commits new work there and requests verification, Duo opens a fresh round on the new HEAD instead of rejecting the request, so the follow-up can still be verified and delivered. A repeated request that still names the already-delivered HEAD is rejected, so a stray resend never reopens a delivered session.
When a round reaches DONE in any mode, Duo asks Austin to summarise the session for you: how the work actually went, and what the repository now has that it did not have before. Austin answers in a normal message, so the report arrives in the timeline as Austin → Human, in bold like the human's own mail. The request is made once per finished round, and the next round reports on itself.
A Goal plan update creates a new version and invalidates both signatures, so wording churn has a visible cost. In Goal, a sign-off is bound to an exact commit: if the peer pushes a new commit, the previous review is stale and must be repeated.
Austin and Tony share a small tool surface installed with the Pi bridge. It is the only channel through which they can affect shared state.
| Tool | Modes | Purpose |
|---|---|---|
duo_send |
all | Send an important live message to the peer while both keep working. For findings, questions, conflicts and proposals — not routine progress chatter. |
duo_set_status |
all | Sign the current gate (ready: true) or revoke your signature (ready: false). In Fast, Austin uses it to request verification while Tony must use duo_set_verification. In Goal, both agents sign every phase. In Step, both approve the queue; Fast steps use Tony's verdict, Goal steps use dual signatures. |
duo_set_verification |
Fast + Step (Tony only) | Report passed or issue_found for Austin's exact commit under review. issue_found requires a concrete note. In Step this applies to a [fast] step; passed lands it and advances the queue. |
duo_set_plan |
Goal + Step | Create or replace the whole shared Plan. Each call makes a new version and resets both signatures. Not available in Fast. |
duo_set_steps |
Step | Create or replace the step queue as a markdown checkbox list, tagging each item [fast] or [goal]. Each call makes a new version and resets both signatures. Duo owns the ticks. |
duo_escalate |
all | Switch the session to Goal mode with a reason. Fast → Goal preserves its escalation-seeded plan; other switches start a fresh Goal round. |
duo_status |
all | Read the authoritative mode, phase, verification status, Plan, step queue and cursor, signatures, and both worktrees' branch, path, HEAD, cleanliness and ahead count. |
Duo Core enforces these gates rather than trusting the model: Fast rejects duo_set_plan, and a Tony duo_set_status in Fast is rejected with guidance to use duo_set_verification. The bridge registers all tools in every mode and holds no project truth of its own, because either agent can switch modes without restarting. Pi events become Duo activity, Pi tools become Duo requests, and Duo messages become Pi steering.
Duo's default and only bundled terminal frontend is the gotui dashboard: duo opens it directly. It combines a split workflow/agent layout with a toggleable chronological timeline; see the gotui frontend guide for its architecture, layout and captures. The old hand-written TUI is preserved on the legacy-tui Git branch for reference, but it is no longer built or installable from main.
duo opens the gotui dashboard; there is no separate UI-selection flag.
The gotui frontend opens in a split dashboard: a workflow sidebar with the phase chain, verification/delivery state, worktrees and two agent cards; Austin and Tony in side-by-side transcripts; and a Duo/Human conversation pane below. F2, the Split / Timeline tabs, or a click on either tab switches to the single chronological timeline.
gotui keyboard shortcuts
| Key | Action |
|---|---|
Enter |
Send composer text to Austin in Fast, or both agents in Goal and Step |
Ctrl+Enter / Shift+Enter* |
Insert a newline in the composer |
F2 / click the Split or Timeline tab |
Switch between the split dashboard and the single timeline |
Ctrl+P |
Show or hide the workflow sidebar |
F1 / Ctrl+/ / Alt+H |
Open Help |
Ctrl+O / Alt+O |
Open session details and the Git diffstat |
Ctrl+M / Alt+M |
Open the model and thinking picker |
Ctrl+G |
Toggle message timestamps |
Ctrl+A / Alt+A |
Attach to Austin's own agent session |
Ctrl+T / Alt+T |
Attach to Tony's own agent session |
Ctrl+] / Ctrl+\ / Ctrl+】 |
Detach from the agent's own session back to gotui |
Ctrl+R / Ctrl+Y |
Restart Austin / Tony if exited or failed |
Ctrl+X |
Copy Austin's latest message to the human to the clipboard |
Shift+Tab |
In the model picker, cycle the selected agent's thinking level directly; the main view does not bind it because the target agent would be ambiguous |
Ctrl+Q / Ctrl+D |
Quit Duo and preserve the session |
← / →, Alt+← / Alt+→ |
Move the composer cursor by character or word |
Home / End |
Move to the start or end of the composer line |
Ctrl+U / Ctrl+K, Ctrl+W |
Delete to line start/end, or delete the previous word |
Backspace / Del |
Delete the preceding or following composer character |
↑ / ↓ |
Move between composer lines; recall task history at the first/last line |
Tab |
Insert four spaces, or autocomplete the highlighted slash command |
PgUp / PgDn |
Scroll the active conversation pane (or an open modal) |
| Mouse wheel / drag | Scroll the pane under the pointer, or select and copy transcript text; a COPIED toast appears briefly in the top-right |
| Click a URL / existing file path | Open the URL in the browser, or show the file in the system file manager |
Esc |
Close an overlay, dismiss the slash menu, or clear a selection/status |
The layout responds to the terminal: the workflow sidebar needs at least 104 columns, the split view needs a 72-column by 16-row main pane, and narrower terminals fall back to the timeline. The sidebar phase chain wraps when it does not fit, and the footer uses compact hints below 80 columns. A resumed session starts with its restored conversation; the startup banner is only added to a new session.
The deterministic tcell captures in the gotui guide show the dashboard, timeline, slash palette, help, details, model picker, confirmation dialog and a narrow terminal.
Typing / in the composer opens an interactive command palette showing available slash commands and descriptions. Use ↑ / ↓ to cycle through selections, Tab to autocomplete the command name, Enter to select (which executes immediate commands or fills in the command prefix for commands that take arguments), and Esc to dismiss the menu.
| Command | Description |
|---|---|
/escalate [reason] |
Switch the current session to Goal mode (shorthand for /mode goal), then choose whether to resume the agents |
/mode |
Print the current session mode |
/mode fast|goal|step |
Switch the session mode, then choose whether to resume the agents. A running round resets to the new mode's first phase; switching in DONE applies the mode to the next task |
/model |
Open the model and thinking level picker (Ctrl+M) |
/overview |
Toggle the session overview and git diffstat (Ctrl+O) |
/preview |
Toggle the gotui workflow sidebar (Ctrl+P) |
/timestamps |
Toggle timeline message timestamps (Ctrl+G) |
/copy |
Copy Austin's latest message to the human to the clipboard (Ctrl+X) |
/help |
Open the full Duo interactive help screen (Ctrl+/) |
/status |
Show authoritative session mode, phase, and worktree info |
/clear |
The composer only holds this command, so it is already empty; Duo forces a full redraw and says so |
/quit or /exit |
Exit Duo and preserve session state |
An unknown command is reported as an error. //text and an absolute path (e.g. /usr/bin/python) are sent as a task (Austin in Fast; both agents in Goal and Step).
Submitted tasks are appended to ~/.duo/history (override with DUO_HISTORY_FILE), so ↑/↓ recall survives restarting or resuming Duo.
The model picker (Ctrl+M, with Alt+M as the fallback when a terminal sends Ctrl+M as Enter) lists the catalog the target's own driver reports for the very installation Duo launched (pi --list-models, agy models, opencode models), so Austin and Tony can sit on different catalogs in the same session. Type to filter, and move with ↑/↓ (or PgUp/PgDn, Home/End). Tab switches the target between Austin and Tony; Space selects a model without closing; Shift+Tab cycles that agent's thinking level immediately in the same dialog; Ctrl+T still switches the Models and Thinking tabs if you prefer the explicit view; Enter applies a model (or advances thinking in the Thinking tab); and Esc closes. Model rows show the canonical level after the model name (off, minimal, low, medium, high, xhigh, max), so a clamped level is visible instead of being translated away. Closing the picker keeps the selected agent. Model and thinking choices are written to .duo/config.json with the agent's driver, so the next launch in that repository loads them again. On pi the switch is live: Pi keeps the conversation and records both the model and the thinking level in the session transcript, so a restart or resume (Ctrl+R/Ctrl+Y) keeps the choice. agy and opencode apply model and effort when the agent starts, so Duo restarts that agent instead — the conversation survives, because the session id is persisted, but in-flight work is interrupted. ▶ marks the picker cursor and ● the target's current model.
Agent output is rendered as lightweight markdown: headings, blockquotes, links and bold/italic/code spans are styled, and markdown tables are drawn with real, aligned borders. A table wider than the timeline is narrowed by wrapping the widest cells instead of truncating them, and a wrapped list keeps a hanging indent so continuation lines stay under the item text.
Mouse selection copies through terminal OSC 52 escape sequences (works seamlessly over SSH, tmux, and modern terminal emulators) alongside native desktop clipboard commands (pbcopy on macOS, wl-copy on Wayland, xclip/xsel on X11).
* A newline is inserted only when the terminal reports the combination distinctly (\x1b[13;2u CSI-u or \x1b[27;2;13~ modifyOtherKeys, which Duo enables). Terminals that send a bare \r for Shift+Enter will submit instead — use Ctrl+Enter, which arrives as \n, if Shift+Enter submits in your terminal. The same limit applies to Ctrl+M: it is reported as the m key with Ctrl (\x1b[27;5;109~ or \x1b[109;5u) only on terminals that honor those modes, and on the others it is indistinguishable from Enter — use Alt+M there.
F1, Ctrl+/ or Alt+H opens the gotui Help modal; scroll with ↑/↓, PgUp/PgDn, Home/End and close with Esc; it is a compact dashboard key and workflow reference. Run duo --help for the full usage, every environment variable and each command's own detail.
Everything has a working default; duo needs no configuration to run.
| Variable | Default | Meaning |
|---|---|---|
DUO_MODE |
fast |
Session mode: fast, goal or step. A CLI --mode/-m wins; duo resume keeps the persisted mode. |
DUO_TEST_COMMAND |
(none) | Automated deterministic test command run before verification / delivery (CLI --test-cmd / -t wins). |
DUO_REPO |
current directory | Repository or subdirectory to launch against. A CLI path argument wins. |
DUO_SESSION |
timestamp + random hex | Session id, formatted YYYYMMDD-HHMMSS-xxxxxxxx. |
DUO_WORKTREE_ROOT |
~/.duo/worktrees/<repo>-<hash>/<session> |
Where the Austin/Tony worktrees are created. |
DUO_BASE_REF |
HEAD |
Ref the worktrees are branched from. |
DUO_PI_COMMAND |
pi |
Command used to launch each Pi agent. |
DUO_DRIVER |
pi |
Default agent driver: pi, agy or opencode. Per-agent flags and config win. |
DUO_LISTEN |
127.0.0.1:0 |
Bridge listen address (an OS-assigned port by default). |
DUO_HARNESS |
true |
Enable the idle/stall watchdog that nudges coordination back to life. |
DUO_HARNESS_IDLE_SECONDS |
15 |
Quiet period before an agent counts as idle. |
DUO_HARNESS_STALL_SECONDS |
300 |
No progress for this long counts as a stall. |
DUO_HARNESS_COOLDOWN_SECONDS |
30 |
Minimum gap between harness nudges. |
DUO_HARNESS_RESUME_GRACE_SECONDS |
45 |
Extra grace after duo resume, so reconnecting is not mistaken for a stall. |
DUO_HISTORY_FILE |
~/.duo/history |
Composer task history recalled with ↑/↓. |
To run a non-default Pi command:
DUO_PI_COMMAND='pi --some-flag' duoDuo hides its own .duo/ directory through .git/info/exclude, so it never
modifies your .gitignore and never leaves your working tree dirty.
Duo drives whichever agent CLI you install. All three speak the same Duo coordination protocol, so the modes, tools, evidence rules and delivery flow are identical — only how Duo launches the agent and observes it differs.
| Driver | Select with | How Duo talks to it |
|---|---|---|
pi (default) |
--agent pi |
A Pi bridge extension connects back over a local socket. Full streaming: tool activity, peer messages and model switching are live. |
agy |
--agent agy |
Google Antigravity CLI. Duo observes the conversation and writes messages into the PTY. Token usage requires sqlite3 on PATH. |
opencode |
--agent opencode |
An opencode plugin connects back over a local socket, the same way the Pi bridge does. |
There is no environment variable for a per-agent driver: DUO_DRIVER sets both,
and to split them use
--austin-driver / --tony-driver or agents.austin.driver / agents.tony.driver
in .duo/config.json.
Pick one per agent to mix them, for example duo --austin-driver opencode --tony-driver pi (or -A opencode -T pi).
duo --agent opencode # both agents on opencode
duo --austin-driver opencode --tony-driver pior persist it per repository in .duo/config.json:
{
"driver": "opencode"
}Two notes specific to opencode:
- Session identity is discovered, not chosen. opencode assigns session ids
server-side, so on a first run Duo lets opencode mint one, learns it from the
bridge, and persists it with the session. Every later run — including
duo resumeafter a crash — passes--session <id>so the agent keeps its conversation. - The model is yours to choose. Duo does not inject a default model for
opencode; it uses whatever your opencode config or catalog already resolves.
A model picked in Duo is passed at startup through opencode's config
environment, because its TUI has no
--modelflag.
Codex is available as --agent codex or per agent with --austin-driver codex
and --tony-driver codex. Duo passes model and reasoning selections to the Codex
CLI, configures the duo MCP server for workflow tools, and runs Codex with
--dangerously-bypass-approvals-and-sandbox so it can work unattended without
folder trust or permission prompts. Codex's sandbox is disabled, so it can access
files outside its worktree; Git worktrees separate normal repository edits but
are not a security boundary. Use this mode only when the host is otherwise trusted
or isolated. Duo passes per-invocation trust overrides for the repository and its
worktree rather than editing Codex's persistent config. Codex may still gate
project-scoped skills. Base commands with conflicting sandbox or approval flags
are rejected.
Codex assigns its own session IDs; the driver learns them from new rollout metadata
for the exact worktree and resumes them with codex resume <id>. If multiple Codex
sessions start in the same worktree at once, the driver leaves the ID unset rather
than risk resuming the wrong conversation. Codex has no live activity or prompt
bridge in this integration; workflow tools are provided through MCP and terminal
input remains available.
Duo runs agents with tool calls auto-approved (--auto for opencode,
--dangerously-skip-permissions for agy) because they work unattended in their
own isolated worktree. duo plugins lists the built-in drivers plus any
duo-plugin-<name> executable (or its legacy duo-driver-<name> / duo-<name>
spellings) found in ~/.duo/plugins/ or on PATH.
The opencode bridge is installed by the install scripts to
~/.config/opencode/plugin/duo/. Restart any running opencode processes after
installing.
An agent CLI that speaks Model Context Protocol can drive the same state machine
through duo mcp-server, which serves the six Duo tools above over JSON-RPC 2.0
on stdio and forwards them to the running session's bridge:
duo mcp-server --agent austin --session <session-id> --token <token> --port <port>
duo mcp-server config --agent tony # print MCP client config JSONConnection parameters default to DUO_AGENT, DUO_SESSION, DUO_TOKEN,
DUO_HOST and DUO_PORT, which Duo sets for the agents it launches, so a
client started inside a session needs no arguments.
duo mcp-server config prints a ready-to-paste mcpServers entry instead of serving.
You can persist settings globally in ~/.duo/config.json or per-repository in .duo/config.json (or .duo.json):
{
"mode": "fast",
"language": "en",
"testCommand": "go test ./...",
"driver": "pi",
"piCommand": "pi",
"agents": {
"austin": {
"command": "pi",
"driver": "pi",
"model": "anthropic/claude-3-7-sonnet",
"thinking": "high"
},
"tony": {
"model": "openai/o3-mini",
"thinking": "medium"
}
},
"harness": {
"enabled": true,
"idleSeconds": 15,
"stallSeconds": 300,
"cooldownSeconds": 30
}
}agents.<name>.command overrides the launch command and agents.<name>.driver
the driver for one agent only; the top-level driver and piCommand set the
defaults both agents start from. Duo passes the selected model and thinking level
to the driver unless the command already carries them; opencode receives the
model through its config environment because its TUI has no --model flag.
Precedence: explicit CLI flags (--mode, --language, --test-cmd, --agent/--austin-driver/--tony-driver) > environment variables (DUO_*) > repository .duo/config.json > global ~/.duo/config.json > built-in defaults. Set language to zh or en; duo --language zh (or duo -l zh) overrides the file. This changes Duo-authored messages and the gotui interface chrome immediately and for later launches; canonical protocol words such as FAST/GOAL/STEP, agent names and phase names stay in their standard spelling.
Duo also writes the repository .duo/config.json: on every launch it records
each agent's active driver and model there, so a model chosen in the picker is
remembered on the next run. A model belonging to a previous driver is dropped
when the driver changes, since its id means nothing to the new CLI. Because
.duo/ is excluded through .git/info/exclude, those writes never show up in
git status.
~/.duo/sessions/<repo-id>/<session-id>/
state.json mode, phase, verification/Plan, signatures, evidence, worktree paths/branches, opaque per-driver resume state
events.jsonl diagnostic journal of phase, signature, bridge and merge events,
plus the pane transcript restored into the TUI on resume
duo.log lifecycle output (session tokens are redacted before writing)
lock advisory flock holding the owner PID and hostname
~/.duo/worktrees/<repo>-<hash>/<session>/
austin/ Austin's worktree — also the integration worktree
tony/ Tony's worktree
~/.duo/config.json global settings you can edit
~/.duo/history composer task history (↑/↓ recall)
~/.duo/plugins/ optional `duo-plugin-<name>` executables
Session directories are created owner-only and describe your project's state; see SECURITY.md.
Duo separates the Git boundary from the agents' default working directory. Launching from a subdirectory keeps the repository as the Git boundary but makes that subdirectory the agents' default cwd:
cd repo/packages/web
duoAustin and Tony then start in the packages/web directory inside their own worktrees, while branches, worktrees and delivery keep their Git boundary at repo. The mapping is done safely against the worktree root, so a scope cannot escape it.
Scope is a default working directory, not a filesystem sandbox. Agents can still reach the rest of the repository when the task genuinely requires it. The scope is recorded in the session and restored on resume, and each agent's active scope is stated in its system prompt.
duo resume # resume this repository's unfinished session
duo resume <id> # resume one specific sessionA plain duo always starts a new session and refuses to overwrite an unfinished one. duo resume picks the only unfinished session if there is exactly one. When several inactive unfinished sessions exist, an interactive terminal shows a numbered choice with each session's phase, scope and update time. In a non-interactive shell, Duo prints the candidates and asks for duo resume <id>.
Resume reloads the persisted snapshot, proves it against Git, and revokes any signature or verification that is no longer provable before starting a process. The mode is persisted with the session: duo resume keeps it, a legacy session with no recorded mode resumes as Goal, and DUO_MODE is ignored on resume (an explicit conflicting --mode is an error). Reconciled state is written back before agents start, so a crash during startup cannot resurrect a revoked approval or replay a merge. Each reconnected agent receives one mode- and phase-aware wake-up message; reconnects do not duplicate it. The most recent 200 entries of each pane are replayed into the TUI, so a resumed session starts with its previous conversation visible.
Recovery is deliberately conservative: when it cannot prove an approval is still valid, it revokes rather than trusts. Expect a re-sign-off after a crash, not a silent pass.
DONE means the final result has been delivered into the repository you launched Duo from — the verified Austin commit in Fast, or the dual-signed integrated HEAD in Goal. Delivery is fast-forward only (git merge --ff-only). Duo never creates a merge commit, rebases, or rewrites your history. If delivery is blocked, duo apply retries it by hand.
A task that changes no repository files is a no-op: once the final commit is already an ancestor of your HEAD, delivery succeeds and records DONE even if the working tree is dirty. For any delivery that would actually move your branch, the safety checks below still apply.
Delivery refuses — and leaves your repository completely untouched — when:
- the repository has uncommitted changes;
- it is on a different branch than the one Duo started on;
- the current HEAD has diverged from the final Duo result;
- HEAD is detached, or the starting branch is unknown;
- the final result is not derived from the recorded base commit.
If you resolve the divergence yourself, Duo recognizes the result instead of refusing: once the final Duo commit is an ancestor of your HEAD — including after a git merge --no-ff that keeps both histories — duo apply treats the delivery as already applied and records DONE without moving your branch again. A refused delivery prints that merge command alongside the retry command:
git merge --no-ff <final-head> # from the repository you launched Duo from
duo applyUntil then, a refused delivery keeps the session in INTEGRATE (Goal) or VERIFY (Fast), records a pending checkpoint, and prints the exact retry command:
duo apply # retry the pending delivery for this repository
duo apply <session-id> # retry one specific sessionduo apply never starts agents and reuses the same safety rules — it will not force a blocked delivery.
Duo persists session snapshots under ~/.duo/sessions/ and isolated Git worktrees under ~/.duo/worktrees/. You can list sessions and reclaim disk space at any time:
duo sessions # list sessions for this repository
duo sessions --all # list sessions across all repositories
duo clean # clean completed (DONE) sessions for this repository
duo clean <session-id> # clean one specific session
duo clean --all # accepted, but plain duo clean already covers every completed session
duo clean --all-repos # clean completed sessions across all repositories
duo clean --force # also clean unfinished sessions (skips active running sessions)
duo clean --dry-run # preview what would be cleaned without modifying disk
duo logs # list the Markdown transcripts written to .duo/logs
duo logs <repository> # list another repository's transcriptsduo clean cleans every completed (DONE) session of the repository by default, so --all currently changes nothing; --force is what adds unfinished sessions. duo sessions accepts -a for --all, duo clean additionally accepts -f for --force and -n for --dry-run, and duo apply <session-id> selects a specific session. Every command prints its own usage with --help (duo clean --help), and duo <repository> --help prints the root usage from inside a repository.
Cleaning a session removes its Git worktrees (git worktree remove --force), prunes the worktree registry, deletes temporary branches (duo/<session>/*), and removes the session snapshot directory. Any session currently locked by an active Duo process is safely skipped.
The remaining commands do not touch sessions: duo plugins lists the built-in drivers plus every discovered external plugin, duo plugins test [<path…>] [--all] [--verbose] checks one or every driver against the Driver Plugin Protocol, duo mcp-server serves the Duo tools over MCP, and duo version (--version) and duo help (-h, --help) print the version and full usage.
Duo supports two fixed roles, Austin and Tony, across switchable Fast, Goal and Step modes. A session cannot recover agent reasoning, and Duo Core does not restart itself after a crash; run duo resume. Session data is local to the machine and repository path. The shell scripts target macOS/Linux. gotui needs a terminal large enough for its split dashboard; smaller terminals fall back to its timeline view, and the retired hand-written TUI remains on the legacy-tui branch. Delivery safety and local TCP trust boundaries are described in architecture and SECURITY.md.
make check runs Go tests, vet and command builds. If you change the Pi bridge,
also run cd pi-extension && bun test. The gotui frontend additionally has a
deterministic SimulationScreen capture test; regenerate its committed frames with:
DUO_GOTUI_CAPTURE_DIR="$PWD/docs/gotui/screens" \
go test ./internal/gotui -run TestCaptureFrames -count=1Pushing a v* tag runs release validation and publishes macOS/Linux archives for
amd64/arm64.
- Architecture and invariants
- gotui terminal frontend
- Driver plugin protocol and development guide
- Bridge protocol
- Security boundaries
- Release history
MIT — see LICENSE.