Tandem lets Claude Code, Codex and agy (Antigravity CLI) work together as one team. A leader agent can spawn workers in any of the three harnesses, give them tasks, and get the answers back in its own session. All agents share one knowledge store and one live project board.
The server is cntctr, a small C11 MCP stdio server. Every agent runs its own copy.
There is no daemon. tmux carries every message, and SQLite holds the shared knowledge.
leader pane (claude) worker window (codex) worker window (agy)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ claude │ │ codex │ │ agy │
│ └ cntctr │ │ └ cntctr │ │ └ cntctr │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
└─────────────► tmux server ◄────────────────────────────┘
pane options, paste buffers, wait-for channels
│
SQLite + FTS5 knowledge store
- Each worker gets its own tmux window, so you can watch it and approve its prompts.
- Messages are pasted into the target pane as an envelope with an id. The receiver
answers with
reply, and the answer goes back to the sender's pane, or it returns directly from a blockingsend. - Concurrent sends to the same target pane are serialized with an advisory per-pane file lock, so multiple agents typing into the same terminal never interleave or corrupt bracketed pastes.
- tmux is called with an argv array and never through a shell. Message text reaches tmux only through a pipe.
- macOS or Linux, with tmux (tested with 3.7c). Run your agents inside tmux.
- json-c (
brew install json-c) and SQLite with FTS5. - At least one of
claude,codexandagyonPATH. - headroom on
PATH. Claude and Codex workers start asheadroom wrap claude|codex -- <args>, so their API calls go through the headroom proxy. agy is started directly. jqto run the tests.sqlite3to runwatch.sh.
make
Register the server once for each harness (use the absolute path):
claude mcp add -s user cntctr -- /path/to/cntctr
codex mcp add cntctr -- /path/to/cntctr
agy mcp add cntctr /path/to/cntctr
Raise each harness's tool timeout above the longest blocking wait you plan to use:
- Claude Code:
MCP_TOOL_TIMEOUTin milliseconds, e.g.export MCP_TOOL_TIMEOUT=3600000. - Codex: in
~/.codex/config.toml, under[mcp_servers.cntctr], settool_timeout_sec = 3600. - agy: no MCP timeout setting found; keep blocking waits short or use async sends.
Start a tmux session, run your leader agent in it, and ask for what you want, for example: "Spawn a codex worker named tests in ./api and have it write tests for the parser. Wait for its answer."
| Tool | What it does |
|---|---|
spawn |
Starts claude, codex or agy in a new tmux window with a first task; optional model and effort |
send |
Types a message into another agent's terminal; wait=true blocks for the reply |
gather |
Dispatches several send or spawn tasks and collects their replies together |
reply |
Answers a message by its id |
list |
Shows agents, their panes, and whether they are alive |
kill |
Closes an agent's pane |
Use gather to collect replies when a decision needs every delegated answer. For example:
{"tasks":[{"kind":"send","to":"tests","text":"Check the tests"},{"kind":"spawn","harness":"codex","name":"review","task":"Review the change"}],"timeout":600}The result is JSON text with complete and one results entry per task in input order. Each entry identifies the agent, message id, and answer or failure (status: "answered", "timed_out", or "failed"). If the shared deadline passes before all answers arrive, complete is false; any late replies that finish after the deadline arrive in the leader's pane with their ids so no output is lost.
spawn and distill pick each worker's model and effort from the task text, so the
leader doesn't have to (cntctr.c:route, tables RULES and ROUTES):
| Tier | Task words | claude | codex | agy |
|---|---|---|---|---|
| light | smoke test, rename, typo, format, lookup, list, summarize, count | claude-haiku-5-5 medium | gpt-6-luna low | gemini-3.8-flash-medium |
| standard | anything else | claude-sonnet-5-5 medium | gpt-6-luna medium | gemini-3.8-flash-medium |
| review | review, audit, verify, merge, critique | claude-opus-4-7 high | gpt-6-sol medium | gemini-3.8-flash-high |
| hard | architecture, design, race condition, concurrency, root cause, security, migration, rewrite | claude-opus-5-5 high | gpt-6-sol high | gemini-3.8-flash-high |
Rules are checked in the order hard, review, light; tasks over
6000 characters count as review. Pass tier, model or effort to override.
Workers run with normal permissions. Approve their prompts in their own windows
(C-b w to switch). cntctr will not type into a pane that is showing a
permission dialog; the sender gets <name> awaiting permission prompt; retry later.
Pane locking: cntctr serializes message injection with an advisory per-pane file lock
(/tmp/cntctr-inject-<uid>-<hash>.lock via flock) across the bracketed paste and Enter key
press. When multiple agents or tools send to the same pane concurrently, each message submits as
its own separate prompt instead of colliding.
Delivery tuning: CNTCTR_ENTER_DELAY_MS (default 150) is the pause between
pasting a message and pressing Enter.
cntctr keeps a knowledge store that every agent shares across sessions. It lives in
SQLite at ~/.local/share/cntctr/kb.db, or at $CNTCTR_DB if that is set.
| Tool | What it does |
|---|---|
note |
Store an insight (1–5 lines), a skill (a how-to) or a doc (long text), for this project or scope=global. supersedes=[ids] replaces older entries. |
recall |
With no query, list the index: this project's entries first, then global ones. With a query, search, including work history and claude-mem observations. |
read |
Show one entry in full. |
forget |
Retire a stale entry. History entries can't be forgotten. |
project |
Show the project key, which is the normalized origin remote, or override it for this repo. |
distill |
Spawn a librarian worker that turns recent history into insights and skills and merges duplicates. |
post |
Type a note into every other live agent working in the same project, and keep it on the project's board. |
board |
Show the project's latest posts, oldest first. |
Every send, spawn and reply is logged as private history. Replies can add
insights=[...]. Every message includes up to 5 matching entries, and every agent gets
its project's index at startup through MCP instructions.
Insights, skills and docs are also copied to claude-mem ($CNTCTR_MEM_URL, default
http://127.0.0.1:37701; set it empty to turn copying off), so recall gets semantic
matches. If claude-mem is down, recall uses keyword search alone.
./watch.sh # in a spare tmux pane; refreshes every 2 s ($CNTCTR_WATCH_SECS)
It shows the live agents, the board, the newest knowledge entries (* = copied to
claude-mem, x = retired), private history, and when the librarian last ran. To check
each feature with real agents, ask your leader to:
notean insight. It appears under knowledge, marked*if claude-mem is running.- Spawn a worker with a task that mentions the insight's words. The task and reply appear
under history, and the worker's window shows a
[cntctr knowledge: ...]block. - Have the worker reply with
insights=[...]. They appear under knowledge. postsomething. It shows up in every other agent's window in the project and on the board.distill. Alibrarian-Nagent appears,last distillupdates, and new or merged entries appear as it works; merged ones turnx.
make test
Tests run against a private tmux server (tmux -L cntctr-test) and need jq.
| File | Purpose |
|---|---|
cntctr.c |
MCP server: JSON-RPC loop, tools, tmux helpers, pane locking, model router |
kb.c, kb.h |
Knowledge store: entries, FTS5 search, history, board |
watch.sh |
Live dashboard |
test.sh |
End-to-end tests with fake harnesses |
docs/superpowers/ |
Design specs and implementation plans |