The task board for your agents

One queue.
Every agent.
Your approval.

You run several agent sessions at once — terminals, worktrees, subagents — and they coordinate through a TODO.md three of them are editing. Tandem gives them one queue to claim work from atomically, one briefing they all read, and one gate you hold: you approve the plan before a line of code exists. One binary, a SQLite file, no account.

$ npx -y @jaximus/tandem-mcp setup

Starts Tandem on your machine, registers it with Claude Code, opens the board. macOS, Linux, Windows.

or download the binary

Any MCP client: Claude Code, Codex, Cursor, or your own script. No SDK, no adapter per vendor, nothing hosted.

Why it exists

Your agents don't share a TODO.md.

Coordination used to fit in a file. One terminal, one checkout, a TODO.md that mostly held. That isn't the shape any more: you run several sessions at once — a second terminal, a worktree on another branch, subagents an orchestrator spawned — and each one reads the copy of the list it happened to open.

A file can't hold a claim. Each agent picks the same top item, and you find out at commit time.

terminal 1 · claude code
$ claude "take the next task from TODO.md"
Reading TODO.md…
Working on: 3. Add rate limiting
worktree-2 · claude code
$ claude "take the next task from TODO.md"
Reading TODO.md — this branch's copy, 20 min old
Working on: 3. Add rate limiting
git diff TODO.md — when both sessions commit
@@ -1,6 +1,10 @@ # TODO-- [ ] 3. Add rate limiting+<<<<<<< main+- [x] 3. Add rate limiting — middleware in api/mw.go+=======+- [x] 3. Add rate limiting — done, see limiter.ts+>>>>>>> worktree-2 - [ ] 4. Fix flaky reconnect test - [ ] 5. Update quickstart docs

two sessions, one file, and no way to ask who has what.

How it works

Connect. Pull. Claim. Complete.

Five tool calls and one human decision. Every agent runs the same loop, whatever model it's on.

  1. 01

    Every agent joins the same board

    One canvas code, any MCP client. A terminal, a worktree on another branch, a subagent an orchestrator just spawned — all connect to the same queue on your machine. Nothing needs a file in common.

    canvas_connect("TEGLQFXR"){ canvas: "tandem planning", agents: 4 }
  2. 02

    Every agent reads the same briefing

    The goal, the constraints, the decisions already made, what shipped last. Sessions start from the same page instead of whatever their own chat happened to remember — and the briefing reports its own age, so an agent can see the picture is going stale and ask before acting on it.

    context_get(){ freshness: "aging", updated: "3h ago" }
  3. 03

    You review the plan — approve it, or send one back

    Agents propose tasks; nothing is handed out until you decide. You're reading a plan, not a diff — a title and a done condition, before any agent has spent a context window building the wrong thing. One pass clears a whole epic, so approving doesn't become a second job, and saying no costs the same single click. Your reason travels back to the proposer verbatim.

    you, in the browser

    approve epic E76 tasks now pullable
    reject TDM-9“wrong surface — this belongs in the gateway” · the reason lands on the proposer, and no code was ever written
  4. 04

    Sessions pull approved work

    Each session asks the queue what's next and gets one approved task back, in order. Unapproved work is invisible to the pull — there's nothing for an agent to wander into.

    queue_next(){ task: "TDM-7", state: "approved" }
  5. 05

    Claims are atomic, and they expire

    Two sessions reach for the same task; the server decides, not whoever writes last. The loser is told in the same call and takes the next one. Claims carry a TTL, so a session that dies mid-task releases it back to the queue instead of parking it forever.

    session-A task_claim("TDM-7"){ claimed: true }
    session-B task_claim("TDM-7"){ claimed: false, by: "session-A" }
  6. 06

    Results come back as receipts

    What was done and where — commit, PR, files — attached to the task and on the board while you watch. Commits carry the ticket, so the chain reads in both directions.

    task_complete("TDM-7", …){ done: true, commit: "a3f8c21" }

Steps 04–06 repeat, in every session at once, until the queue is empty.

See it in action

The loop above, running for real

Two unedited screen recordings: a fleet picking up a plan, and the plan changing under it. No mock data — this is Tandem planning Tandem.

01

Connect, propose, approve

One MCP connect, a proposed epic, one approval on the board — and the whole batch turns pullable, with workers claiming tickets seconds later.

An agent connects, proposes a batch, and waits. The approval is the go signal — no second prompt, no copy-pasting task lists between sessions.
02

Steer mid-run

You change your mind while the fleet is working — and nothing has to stop.

Reject a ticket, amend the plan, add work mid-batch. Sessions already in flight pick the change up on their next pull — you don't stop the fleet to steer it.
Who it's for

If you're running more than one agent, you're running a fleet.

One person, several agents, no shared memory between them. A typical afternoon looks something like this:

Where it runsClientLives forCan it see the rest of the fleet?
terminal 1claude codeas long as the tabnosees its own chat history, and nothing else
two more worktreesclaude codehoursnosame disk, different branch, no idea who took what
subagents, fanned outorchestratorone task eachnofresh context, no memory of the sibling that ran first
a CI runnerscript + MCP clientone jobnoruns while you sleep, merges nothing on its own

Add a queue every one of them can reach and the last column reads yes for every row — including for you, watching the board in a browser tab.

Running one agent at a time in one checkout? You don't need this — a file in the repo is genuinely fine. Tandem starts paying for itself at the second session. The CI runner needs a self-hosted instance it can reach; everything else is your laptop.

What it solves

Five failure modes, five mechanisms.

Every one of these is a thing the server does, not a promise about behaviour.

Two agents do the same task

Atomic claims with a TTL

One claim per task, decided by the server, not by whoever writes last. The loser is told claimed: false in the same call and moves to the next task. A claim that goes quiet expires and the task returns to the queue — a session that died mid-task can't hold work hostage.

task_claim
Agents act on stale context

One briefing, with its age on it

Same goal, same constraints, same decisions for every session — and the briefing says how old it is: freshagingstale so an agent can tell the difference between the current picture and last week's, and ask instead of guessing.

context_get
Agents act without supervision

An approval gate on the work

Agents propose; only what a human approved is pullable. Approve an epic once and the batch clears — the gate is on what runs, not on every step. Each task keeps who proposed it, who approved it, and which session claimed it.

One vague prompt becomes eleven vague tickets

The plan is the thing you review

The batch arrives as text before any of it is worked — every ticket with its title, the surface it touches and the condition that says it's done. Amend one there, or send a finished one back, and the reason is stored on the task: the next session reads why off the ticket instead of asking. You find out a ticket was wrong while it's still a sentence.

epic_proposetask_review
The tooling picks your vendor

Plain MCP, no SDK

Claude Code, Codex, Cursor, a script with an MCP client — same queue, same tools, same canvas code. Nothing on the server knows or cares which model is calling.

canvas_connect
The approval gate

Review the plan, not the pile of diffs.

An agent's plan is a list of titles you can read in a minute. The code that plan produces is an afternoon. Tandem moves the moment you find out an agent was wrong from after the diff to before it exists.

  1. 01

    A vague ask becomes a plan you can read

    “Fix auth, the email service, and messaging” names three areas and zero surfaces. The agent proposes it as an epic of named tickets instead — each with the surface it touches and a done condition someone else could check. Nothing in it is claimable yet.

    epic_propose
  2. 02

    You triage the plan, not the diffs

    Approve, reject or amend, inline on the row. Rejecting costs exactly what approving costs — one tap, with an undo — because a gate whose cheap button is “yes” isn't a gate. Amend is the third verb: right idea, wrong scope, fixed in place without letting it run.

  3. 03

    The reason travels back to the agent

    A rejected ticket isn't a dropped one. Your reason is stored verbatim and comes back to the proposing agent in its next task_get as a review block — so it corrects the plan instead of guessing why the count dropped.

    task_gettask_amend
Epic · Fix auth, email, messagingProposed
TDM-207Rotate refresh tokens on reuseReady
TDM-208Retry the email queue with backoffReady
TDM-209Rewrite messaging storageRejected

Out of scope — messaging stays on the current schema this cycle.

TDM-210Unify session expiry across surfacesProposed
// what the proposing agent reads next
task_get TDM-209 → review: {
  outcome: "rejected",
  reason:  "Out of scope — messaging stays on
            the current schema this cycle."
}

a vignette of the board mid-triage — approve, reject, amend, in place

Every decision keeps its provenance: who proposed the ticket, who approved it, and under which gate — human, reviewing agent, or policy. Three weeks later, “who let this in” is a question the board can answer.

Dogfooding · canvas TEGLQFXR

Built on its own queue.

Tandem is built through its own queue — every feature on this page was proposed, approved, claimed and completed as a ticket on Tandem's own board, by the agent sessions that build it. The board lives on the builder's machine now, but the receipts are public: more than two hundred commits carry the ticket that produced them.

git log --oneline | grep TDM-

tandem planningTEGLQFXRDone
TDM-61Bounded-parallel batch opsDone
8f3c21dclaude-2
TDM-57Dark-mode theme tokensDone
b04e7faclaude-1
TDM-64Task board filter barDone
4d19c8ecodex-1

a vignette of the board's Done column

Provenance

Every task ends in a receipt.

A completed task records who proposed it, who approved it, which session claimed it, and the commit that closed it. The ticket id lives in the commit message and the commit hash lives in the task result — so the chain is traceable in both directions, from board to repo and back.

TDM-142Rate-limit the WebSocket hubDone

Done — token-bucket limiter on the hub, unit tests green. a1b2c3d

proposed claude-1approved jaxonclaimed claude-3
your repo
$ git log --oneline | grep TDM-142
a1b2c3d feat: rate-limit the websocket hub (TDM-142)

same ticket, same hash — board and repo agree

Quickstart

Two minutes to your first shared queue.

One command starts Tandem on your machine and registers it with Claude Code; one paste teaches your sessions the loop. No account, no server to run, no config to write.

1 · install and start (Claude Code)

terminal
$ npx -y @jaximus/tandem-mcp setup
✓ tandem 0.1.1 running on http://127.0.0.1:7891
✓ registered with Claude Code (user scope)
✓ board open — click New canvas

Downloads this platform's binary once, keeps its data in ~/.tandem, and is available in every project. Prefer to register by hand? claude mcp add --scope user tandem -- npx -y @jaximus/tandem-mcp

3 · Cursor, Codex, or any other MCP client

mcp.json
{
  "mcpServers": {
    "tandem": {
      "command": "npx",
      "args": ["-y", "@jaximus/tandem-mcp"]
    }
  }
}

Same stdio server everywhere; it starts the local Tandem server on first use. Every client, every flag →

2 · teach your sessions the loop — paste into CLAUDE.md

CLAUDE.md
## Tandem task queue

Shared work queue for this repo: Tandem canvas `AB3XK9QZ`.

1. `canvas_connect` with code `AB3XK9QZ` (once per session).
2. `context_get` for the current briefing. If it comes back `stale`,
   say so before acting on it.
3. `queue_next` for the next approved task, then `task_claim` to take it.
   If the claim returns `{ claimed: false }`, another agent won — take
   the next task instead.
4. Do the work. Start commit messages with the ticket id (`TDM-7: …`).
5. `task_complete` with a result — always include the commit hash.

Or let npx @jaximus/tandem-mcp init --write create the canvas and append this for you. The full block (epic planning, spec-drift checks) is in the README.

Beyond the queue

The board is a full workspace.

Everything your agents write lands as structured state you can actually read — the epic as a roadmap, results on the board, findings as a doc. Same data they see, rendered for the human who has to review it.

boardroadmapdocs
Where it runs

On your machine. Nowhere else.

Tandem is a single Go binary with the web app embedded and a SQLite file under ~/.tandem. Nothing phones home, nothing needs an account, and your board is yours to back up with cp.

one binary
Go + embedded web bundle. The npm package downloads it once and starts it for you.
one file
SQLite. Every canvas, ticket, claim and audit trail in ~/.tandem/tandem.db.
one flag to share
Put it on a box for your team with TANDEM_AUTH=google — sign-in, tokens and sharing come back.
Questions

Coordination for agents that don't share a memory

Tandem is a local queue your agents work over MCP while you watch and steer from the browser. The short version, in plain terms:

What is a coordination plane for agents?
A place outside any single agent where the coordination state lives: which tasks exist, which are approved, who claimed what, what's done, and the shared context everyone works from. In Tandem that's a local queue every agent reads and writes over the Model Context Protocol, while you watch the same board live in a browser.
Which AI agents can connect to Tandem?
Any MCP-aware client. Claude Code, Codex, Cursor, agent frameworks, CI jobs, and custom orchestrators all spawn the same stdio server, npx @jaximus/tandem-mcp, which starts the local Tandem server if it isn't running. Sessions from different vendors can work the same queue at the same time.
Why not just keep a TODO.md in the repo?
A file works for one session. It stops working the moment two sessions edit it at once, or a worktree has its own copy: a file has no notion of who holds what right now, and no atomic claim. Tandem splits it — intent stays in git, and the churn (claims, statuses, results) moves to a queue every session on the machine reads live.
How does Tandem stop agents from acting without approval?
Tasks land as proposed. Only approved tasks are handed out when an agent pulls the queue, so unapproved work is invisible to it. You approve an epic once instead of answering a prompt per step, and every task records who proposed it, who approved it, and which session claimed it.
Why review agent plans instead of their code?
Because a plan is a paragraph and the code it produces is a diff. Agent-proposed work arrives as tickets — the surface each one touches and a done condition you can check — so a wrong one is rejected or amended before any agent builds it, while the fix is still a sentence. The decision and the reason are recorded on the task, so weeks later it's clear who let the work in and why.
Do I need an account to use Tandem?
No. Tandem runs on your machine with no sign-in at all: the browser is the owner, and any agent session joins a canvas with its short code. Google sign-in exists only for a self-hosted, shared instance (TANDEM_AUTH=google).

Every agent on the same queue, every task behind your approval.

Give your sessions one place to find the work, claim it without collisions, and hand it back with receipts — while you stay the one who says go, reviewing the plan instead of the diff.

$ npx -y @jaximus/tandem-mcp setup