pair-mailbox MCP server
Commit Nov 14, 2026Enables two independent AI coding agents to collaborate through an autonomous pair programming protocol with a strict single-writer invariant.
Also known as AVP-Dev/pair-mailbox
- Stars
- 1
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Nov 14, 2026
- License
- MIT
What is pair-mailbox MCP?
pair-mailbox enables two independent AI coding agents to collaborate through an autonomous pair programming protocol with a strict single-writer invariant.
Summary generated from the project's README
About pair-mailbox MCP server
Developers can transform two independent AI coding agents into a disciplined pair programming team using pair-mailbox. This system enables one agent to act as a writer while a second agent serves as an adversarial critic that reviews code, runs tests, and proposes changes. By using this protocol, agents can collaborate across different command-line interfaces like Claude Code, OpenAI Codex, or Antigravity without requiring a human to manually relay messages between terminal tabs.
The server enforces a strict single-writer invariant to prevent merge conflicts, ensuring only one agent has authority to edit files. The critic agent submits changes via SHA-256-sealed patches that the writer must verify before application. Running on Linux or POSIX systems, the setup requires Python 3.8+, tmux, and git, but it uses zero external pip dependencies. It manages the entire lifecycle from scoping and planning to testing and final pull requests through autonomous modes.
What you can do with it
- 1Autonomous pair programmingAssign a complex coding task and let two agents work through the entire development lifecycle until a pull request is ready.
- 2Adversarial code reviewUse a second agent to independently break hypotheses and catch flaws in the primary agent's code before it is shipped.
- 3Automated CLI unblockingAllow the agents to bypass interactive terminal prompts like permission requests or yes/no confirmations without human intervention.
Written from the project's README. Every use case is checked against a quote from it.
README
View on GitHub⏱️ In 10 Seconds: What Changes? (Before & After)
| Without pair-mailbox (Single Agent / Manual Chat) | With pair-mailbox (Autonomous Pair) |
|---|---|
| ❌ Hallucinates and agrees with itself: agent writes bugs, writes flawed tests, and tells you "all green". | 🛡️ Adversarial Critic: second agent independently breaks hypotheses, runs tests, and catches flaws before shipping. |
| ❌ You act as a manual copy-paste messenger between two browser/terminal tabs ("human relay"). | ⚡ Autonomous peer channel: agents communicate, wake each other in tmux, and coordinate without you. |
| ❌ Both agents overwrite each other's files, creating merge conflicts and broken states. | 🔒 Strict Single Writer: exactly one agent edits files; critic proposes verified SHA-256 sealed patches. |
❌ Stalls indefinitely on interactive CLI prompts ([y/N] or Allow execution?). |
🤖 Safe Autonomous Unblocking: watchdog and peer safely confirm allowlisted commands. |
| ❌ You spend hours supervising. | 🎯 Give task & accept PR: pair drives Scoping → Plan → Code → Review → Test → PR under auto. |
🚀 Quick 3-Step Demo
# 1. Install once across detected CLIs
./install.sh
# 2. Start pairing from your project repo
pairup --task "fix retry jitter handling in client"
# 3. Done. The pair collaborates in tmux and opens a verified PR.
⚡ Why pair-mailbox?
An AI agent working alone is susceptible to its own assumptions: it gladly accepts its own reasoning, skips adversarial verification, and writes tests that validate its own misunderstandings.
pair-mailbox solves this by pairing two independent CLI agents (running different models, different prompts, and completely isolated context windows) on the same codebase under strict, server-enforced engineering invariants:
- Strict Single Writer Protocol Invariant: Exactly one agent holds write authority (
pair_claim). The MCP server strictly enforces the protocol boundaries: it refuses patch reservations from the writer (who edits directly), forbids the critic from verifying patches or advancing phases, and logs all refused attempts to the audit transcript. Agents' instructions and dynamic runtime guards (pair-guard) ensure the reviewer remains read-only and submits changes exclusively via sealed patches. - SHA-256-Sealed Patches (hash seal, not a signature): The reviewer proposes changes through isolated patch files (
0600), sealed with SHA-256 hashes that the writer verifies immediately before applying. - The Mailbox Is the Guarantee, Delivery Is the Accelerator: A turn ends with a bounded
pair_read—pair_read(wait_seconds=N), one read per turn. Nothing in any MCP implementation can inject a turn into another agent's client: MCP is client-driven, the server answers requests and cannot push the host. So the only universal wake-up is the agent reading its own mailbox, and an agent that ends its turn this way is never idle with unread mail — it is either reading or it already has the message. An empty answer means nothing new arrived: go back to work or sleep, the peer and the supervisor wake you. The body always lands in the mailbox;pair_delivertypes only a short pointer into the peer's pane and presses Enter, which is faster where the composer is visible, but a full-screen TUI hides it, so delivery is best-effort and the mailbox always works. The human user never acts as an RFC 1149 messenger copying text between windows. - Autonomous CLI Permission Unblocking (
pair_unblock): Interactive prompts like[y/N]orAllow command execution?inside tmux are automatically detected and approved by the peer or background watchdog (pairwatch --unblock), eliminating unattended stalls. - Atomic, Crash-Resilient State & File Locking: Monotonic numbering (
000001-<UTC-stamp>-<role>-<token>.json, numeric prefix sorts chronologically), atomic file writes (replace), and advisoryfcntl.flocklocks preventing race conditions between concurrent CLI processes. Full append-only audit trail logging every message, claim, phase change, and rejection. - Zero Pip Dependencies: Pure Python 3 standard library. Runs on Linux and POSIX systems with
tmuxandpython3(8+) with zero pip packages to install;tmuxandgitare required system tools.
🏛️ System Architecture
graph TD
subgraph Agents ["Two Independent Agent CLIs"]
subgraph CLI_Writer ["Writer CLI (e.g. OpenCode / Antigravity)"]
AgentA["Writer Agent (LLM 1)"]
ToolsA["pair_* MCP Client"]
end
subgraph CLI_Critic ["Critic CLI (e.g. Codex / Claude Code)"]
AgentB["Critic Agent (LLM 2)"]
ToolsB["pair_* MCP Client"]
end
end
subgraph CoreEngine ["pair-mailbox Engine (Python 3 stdlib)"]
Server["MCP Server (JSON-RPC 2.0 / stdio)"]
Authority["Invariant & State Enforcer"]
Composer["tmux Composer Injector<br/>(pair_deliver / pair_unblock)"]
end
subgraph Storage [".pair/ Storage SSOT (Atomic & Monotonic)"]
State["state.json<br/>(writer, phase, mode, round, roster)"]
Mailbox["mailbox/<role>/<br/>(seq-monotonic + UUID)"]
Patches["patches/<br/>(0600 mode, SHA-256 seal)"]
Notes["notes/<br/>(plan, verdict, finding, handoff)"]
Oblig["obligations/<br/>(expects_reply, review queue)"]
Notif["notifications/<br/>(per-role event hints)"]
Transcript["transcript.jsonl<br/>(Append-only Audit Log)"]
end
AgentA <--> ToolsA
AgentB <--> ToolsB
ToolsA <--> Server
ToolsB <--> Server
Server --> Authority
Authority <--> State
Authority <--> Mailbox
Authority <--> Patches
Authority <--> Notes
Authority --> Transcript
Composer -. "Active Input & Enter" .-> CLI_Writer
Composer -. "Active Input & Enter" .-> CLI_Critic
style CLI_Writer fill:#121218,stroke:#10b981,stroke-width:2px,color:#fff
style CLI_Critic fill:#121218,stroke:#f59e0b,stroke-width:2px,color:#fff
style CoreEngine fill:#181824,stroke:#8b5cf6,stroke-width:2px,color:#fff
style Storage fill:#121218,stroke:#3b82f6,stroke-width:2px,color:#fff
🚀 Quickstart
📋 Prerequisites
- Python 3.8+ (standard library only — 0 external runtime pip dependencies).
- tmux (
sudo apt install tmuxon Debian/Ubuntu orbrew install tmuxon macOS) — required for split sessions, activepair_deliverdelivery, andpair_unblockpermission handling. - bash 4+ (macOS ships 3.2:
brew install bash— the file-size gate uses associative arrays) — required for split sessions, activepair_deliverdelivery, andpair_unblockpermission handling. - Any two AI CLI agents from supported profiles (or your own custom CLI registered in
cli-profiles.json).
1. Installation
Clone and install into your local environment:
git clone https://github.com/AVP-Dev/pair-mailbox.git
cd pair-mailbox
./install.sh
install.sh automatically detects all supported CLIs present on your machine, configures their MCP settings, installs skills and prompts, and validates the connection with a live handshake test.
./install.sh --list # Inspect detected CLIs
./install.sh --check # Run non-destructive verification
./install.sh codex # Install to a specific CLI only
2. Supported CLI Profiles
| Profile | Application | MCP Config Location | Agent Definition |
|---|---|---|---|
opencode |
OpenCode | ~/.config/opencode/opencode.jsonc |
~/.config/opencode/AGENTS.md |
agy |
Antigravity CLI | ~/.gemini/config/mcp_config.json |
~/.gemini/AGENTS.md |
codex |
OpenAI Codex CLI | ~/.codex/config.toml |
~/.codex/AGENTS.md |
claude |
Claude Code | ~/.claude.json |
~/.claude/CLAUDE.md |
pi |
Pi Coding Agent | ~/.pi/agent/mcp.json |
~/.pi/agent/AGENTS.md |
omp |
OMP Coding Agent | ~/.omp/agent/mcp.json |
~/.omp/agent/AGENTS.md |
pairup --swap supports all six profiles (opencode, agy, codex, claude, pi, omp). To add any custom CLI: append an entry to cli-profiles.json and run ./install.sh <name>.
3. Launching a Pair Session
From your project directory — one command, seconds:
pairup --install # once: puts a shim into ~/.local/bin
cd ~/work/<project> && pairup --task "fix 502 retry handling and backoff jitter"
If pairup: command not found: the shim is either not installed or
~/.local/bin is not on PATH. Fix once:
./bin/pairup --install # from inside the pair-mailbox clone
echo $PATH | tr ':' '\n' | grep .local/bin # empty on macOS by default
If it's empty, add to ~/.zshrc (macOS/zsh) or ~/.bashrc (Linux/bash):
export PATH="$HOME/.local/bin:$PATH", then source ~/.zshrc. From now on
pairup runs from any directory. First time you may also just use the full
path: cd <pair-mailbox-clone> && ./bin/pairup.
pairup finds .pair by walking up from cwd, starts the supervisor if it is
dead, and raises the writer + critic from the standing arrangement. Only the
writer is pinned (bin/pairctl roles --writer opencode --global); the critic
is any second live role, so a CLI whose limits ran out is swapped with
pairup --swap <cli> and no paperwork — writer and authority untouched.
Manual form (the same thing pairup does for you), in two tmux panes:
Pane 1 (e.g. OpenCode / Writer):
opencode --standalone --auto
Pane 2 (e.g. Codex / Critic):
codex
In either window, simply state the goal:
work with the peer on: fix 502 retry handling and backoff jitter
The agents will read the codebase, claim roles (pair_claim), enter the autonomous pairing loop (Plan → Implement → Sealed Review → Test → PR), and finish the task without interrupting you.
4. One-command setup (finish.sh)
The heavy first-time path: full pipeline in 7 steps — (1) tests before install, (2) install, (3) standing roles, (4) autonomy, (5) supervisor before agents, (6) agents in fresh tmux windows, (7) status report. Takes 5–10 minutes (full test suites); for the daily start use pairup above.
./finish.sh --task "fix 502 retry handling and backoff jitter"
./finish.sh --no-pair # install and verify, do not launch agents
No positional task argument: the task goes only through --task. See the header of finish.sh for the step order and rationale.
🛠️ CLI Operations & Monitoring
Keep full visibility into your multi-agent workflow:
pairup # Fast start from the project folder: supervisor-if-dead + pair (daily)
pairup --swap agy # Critic's limits ran out: new critic pane, writer untouched
pairup --worktree <branch> # Run pair in an isolated git worktree (-w alias)
bin/pairctl worktree list # List active git worktrees across the repo
bin/pairctl budget # Inspect round budget ceilings (advances, messages, restarts)
bin/pairchat # Live streaming conversation with colored rejections (Ctrl+C to exit)
bin/pairchat --once # Print current project chat snapshot and exit
bin/pairchat --all --once # Cross-project telemetry: stream chats from all active workspaces
bin/pairctl all # Table of all paired projects, current writers, and silence timers
bin/pairctl status # Inspect current workspace state JSON
bin/pairctl why # View active writers and their declared technical rationale
bin/pairpilot # Supervisor: keeps the pair alive unattended (see below)
bin/pairpilot --once # One supervisor pass, to see what it would do right now
bin/pairpilot --launch agy=agy # How to restart a role whose process died
bin/pairwatch --unblock # Background daemon to detect stalls and auto-approve [y/n] prompts
bin/pairdoctor # Health check and environment diagnostic utility
bin/pair-duo --swap-critic agy --state <dir> --project <dir> # Swap critic directly
bin/pair-duo agy # Two instances of ONE CLI: agy-writer and agy-critic
bin/pair-duo <cli> runs two copies of the SAME CLI in one project — the writer
gets the role <cli>-writer, the critic <cli>-critic, each derived from the
role the CLI's own MCP config gives. A foreign role is refused: the environment
may only name an INSTANCE of the configured role, never replace it.
🧭 How far the pair runs without you
bin/pairctl autonomy <level> (default auto). This axis is separate from the round
mode above, because one asks what the round is for and the other how far it goes alone:
| Autonomy | Restarts a dead agent | Confirms permission prompts | Closing a round |
|---|---|---|---|
auto (Default) |
yes | only from the permission allowlist | needs recorded evidence; no word needed — the round closes itself |
semi |
yes | only from the permission allowlist | needs recorded evidence and one word: bin/pairctl ok |
observe |
no | no | human is driving; proof is still required |
Evidence is required in every level. An earlier version let observe close a round
with nothing recorded, which made the most cautious setting the only one where a round
could end on nothing at all — a side effect dressed up as a decision. observe means
"do less", not "do not count".
Picked once, at launch
The level is chosen when the pair starts and not renegotiated mid-round, so you are
asked at most once — and in auto, not at all.
pairup --task "autonomy: semi fix the thing" # or pairctl autonomy semi
With no flag, the launcher resolves in this order, strongest first:
| Source | What it looks like |
|---|---|
task |
autonomy: semi on the first line of the task |
file |
.pair/autonomy, one word |
default |
auto |
A malformed flag is a usage error (exit 2), not a fall-through — a level the
human did not choose must never start silently. A malformed task line or file
falls through to the next source: refusing to start is worse than starting at a
level the human can see in pairctl status.
Secrets, ports, deploys, migrations, data deletion and git history stop the pair at
any level, auto included (PROTOCOL.md §12).
bin/pairpilot is the process that watches the pair while both agents are idle. It delivers
deferred messages, nudges a silent agent, confirms blocked permission prompts from an
allowlist only (DEFAULT_ALLOW_PATTERNS in mcp/pair_mailbox/composer.py: reads, in-project edits, tests/linters, local git, git push to a feature branch, gh pr — with NEVER_ALLOW_PATTERNS refused at any setting), restarts an agent whose process died while its
window lives, reports sealed patches nobody applied, writes a checkpoint digest for the
human in semi, writes a report when the round closes, automatically parks itself on done to prevent token quota waste (pilot_parked), issues smart preventive /compact, and audits runtime in-memory server freshness (server_freshness).
It cannot move phases, grant or revoke write authority, commit, apply a peer's patch, or touch project files. Those are the boundaries that make the arrangement safe (PROTOCOL.md §13).
🔒 What the supervisor may press "y" on
Only prompts matching the allowlist. The default list is DEFAULT_ALLOW_PATTERNS (mcp/pair_mailbox/composer.py:909-975): reads, in-project edits, tests and linters, local
git, git push to a feature branch, and gh pr (create / issue / run / workflow). That is deliberate: a read-only
default would stall the pair on every step, which is the exact failure this was built to
remove.
Refused at any allowlist setting:
- everything in PROTOCOL.md §12 — secrets, ports/deploy, DB migrations, deleting data, production;
- destroying the machine:
rm -rf /,rm -rf ~,rm -rf .,mkfs,dd if=,killall; - git history and the default branch: push to
main/master,--force,reset --hard,clean -f; - network and package installs:
curl,wget,pip install,npm install— not because networking is unsafe by itself, but because it is how a protected file leaves the machine; - piping into a shell:
| sh,| bash,$(,find … -exec, which turn any read into execution.
Refusals are reported, not silent: pilot_unblock_refused goes to the transcript with the
reason, and the pane stays blocked for the human.
Widen or narrow it yourself:
echo '{"allow": ["Read", "grep", "git status"]}' > .pair/permissions.json
The decision is made on the prompt text, not by parsing the command, so a harmless word can sit next to a dangerous one. Every approval is written to the transcript together with the exact line that was allowed — so the journal says what got a "yes".
🔄 Dual Execution Modes
Switch modes at runtime with a single command (bin/pairctl mode <mode>):
| Mode | What Happens | Code Changes | Patch Flow | Purpose |
|---|---|---|---|---|
fix (Default) |
Writer edits, critic proposes sealed diffs, tests run, git PR opened | Yes | Active (reserve → seal → verify) |
Ship bugfixes, features, and refactoring end-to-end. |
audit |
Both agents systematically hunt defects and try to refute each other's claims | Zero | Refused by server | Rigorous vulnerability / race condition audits. Verified findings land in docs/backlog/. |
🧰 MCP Tool Reference
pair-mailbox exposes 22 specialized MCP tools:
| Category | Tool | Authorized Roles | Description |
|---|---|---|---|
| Identity & State | pair_state |
Both | Returns current role, write authority status, task, phase, mode, and peer status. |
pair_claim |
Both | Claims write authority with a required technical rationale. Rebuffs second claims. | |
pair_release |
Writer | Voluntarily releases write authority with a recorded reason. | |
pair_advance |
Writer / Both | Advances phases forward (writer); rolls phases back (both — critic's veto power). |
|
pair_ping |
Both | Cheap liveness heartbeat: moves last_seen and answers with three fields instead of the full state. |
|
pair_digest |
Both | Token-budgeted orientation digest (task, phase, open tasks, obligations, last commit/verdict) instead of reading full history. | |
| Communication | pair_deliver |
Both | Active delivery: body goes to the peer's mailbox, only a short pointer is typed into its tmux composer (best-effort; mailbox always works). |
pair_send |
Both | Passive delivery: Sends a message to the peer's atomic mailbox. | |
pair_read |
Both | Reads unread messages with optional blocking wait (wait_seconds=45), one read per turn. A batch stays unread until its ack_token is passed; the answer names must_ack and letters_behind. |
|
pair_note |
Both | Leaves persistent notes (plan, decision, finding, verdict, handoff). |
|
pair_note_index |
Both | Lists every note with its size and entry count, largest first, so an agent can decide what to read without spending tokens on content. | |
pair_note_read |
Both | Reads the TAIL of a note, cut at an entry boundary and capped at 400 lines. Full-note reads are how a 19k-token verdict used to be paid for on every round. | |
pair_task |
Both | Adds to the shared task list, marks done, or drops with a required reason. The list is append-only: only the writer closes a task, and an open task blocks the round. | |
| Safety & Patches | pair_patch_reserve |
Critic Only | Prepares a dedicated, mode 0600 patch file. Refused for writers and audit mode. |
pair_patch_seal |
Critic Only | Computes and securely records SHA-256 digest of the unified diff. | |
pair_patch_verify |
Writer Only | Re-computes and verifies SHA-256 before applying. Halts immediately on mismatch. | |
pair_propose |
Critic Only | One call instead of six steps: the server builds the unified diff itself (git diff HEAD over the worktree files). Refused for writers and audit mode. |
|
pair_patch_applied |
Writer Only | Records whether the apply succeeded. An unapplied sealed patch blocks closing the round. | |
pair_patch_release |
Both | Deletes a patch file by exact absolute path (globs strictly prohibited). | |
pair_evidence |
Both | Records an agent-reported fact for this round (tests, lint, typecheck, commit, PR) — self-declared by the agent, the close gate trusts it. semi/auto refuse to close a round without it. |
|
| Autonomy & Audit | pair_unblock |
Both | Inspects peer tmux pane for CLI prompts ([y/N], Allow execution?). Confirms only allowlisted prompts and reports refusals. |
pair_transcript |
Both | Inspects full append-only audit trail including every rejected operation. |
pair-mailbox MCP questions
What are the system requirements?
You need Python 3.8+, tmux, and git installed on a Linux or POSIX system.