tandem#
⚠️ Experimental software. tandem is a working prototype — the RPC protocol, on-disk format, and CLI surface may change. Don't use it for data you can't regenerate. Back up your repos.
jj workspaces over the network. One server, many agents on many machines, real files.
Install#
Published on crates.io as jj-tandem.
Requires a Rust toolchain and the Cap'n Proto compiler.
cargo install jj-tandem
Or build from source:
git clone https://github.com/laulauland/tandem.git && cd tandem
cargo build --release
Quickstart#
# On your server (VPS, or localhost for testing)
tandem up --repo ~/project --listen 0.0.0.0:13013
tandem status
# On each agent's machine
tandem init --tandem-server=your-server:13013 ~/work
cd ~/work
echo 'pub fn auth() {}' > auth.rs
tandem new -m "feat: add auth"
That's it. The agent is now using jj against the remote store — tandem log,
tandem diff, tandem file show, tandem bookmark all work because tandem
implements jj-lib's store traits as RPC stubs. The server holds a real jj+git
repo, so jj git push on the server ships to GitHub.
Deployment#
On a VPS (recommended)#
The default setup. Server on a VPS, agents connect from their machines.
# SSH to your VPS, install tandem
cargo install jj-tandem
# Start the server
tandem up --repo /srv/project --listen 0.0.0.0:13013
# Verify
tandem status
On agent machines:
# Agent A
tandem init --tandem-server=your-vps:13013 ~/work
cd ~/work
echo 'pub fn auth(token: &str) -> bool { !token.is_empty() }' > auth.rs
tandem new -m "feat: add auth module"
# Agent B (different machine)
tandem init --tandem-server=your-vps:13013 --workspace=agent-b ~/work
cd ~/work
tandem log # sees Agent A's commit
tandem file show -r <change-id> auth.rs # reads Agent A's file
echo 'pub fn api() -> &str { "ok" }' > api.rs
tandem new -m "feat: add API handler"
Ship via git from the server:
# On the VPS
cd /srv/project
jj bookmark create main -r <tip>
jj git push --bookmark main
The server is a real jj+git repo. jj git push just works.
Local testing#
Server and agents on the same machine, different directories.
# Start server
tandem up --repo /tmp/project --listen 127.0.0.1:13013
# Agent A
tandem init --tandem-server=127.0.0.1:13013 /tmp/agent-a
cd /tmp/agent-a && echo 'hello' > file.txt && tandem new -m "agent A"
# Agent B
tandem init --tandem-server=127.0.0.1:13013 --workspace=agent-b /tmp/agent-b
cd /tmp/agent-b && tandem log # sees agent A's commit
# Done
tandem down
Docker#
Containers connecting to a server. Use tandem serve (foreground mode) —
appropriate for container entrypoints.
docker network create tandem-net
# Server container
docker run -d --name tandem-server --network tandem-net \
-v $(pwd)/target/release/tandem:/usr/local/bin/tandem \
debian:trixie-slim \
tandem serve --listen 0.0.0.0:13013 --repo /srv/project
# Agent container
docker run --rm --network tandem-net \
-v $(pwd)/target/release/tandem:/usr/local/bin/tandem \
debian:trixie-slim bash -c '
tandem init --tandem-server=tandem-server:13013 /work
cd /work
echo "from agent A" > hello.txt
tandem new -m "agent A commit"
tandem log --no-graph
'
docker stop tandem-server && docker rm tandem-server
docker network rm tandem-net
With Claude Code / AI agents#
Each agent gets its own tandem workspace. They see each other's work in real time via the shared store.
# Server (your VPS)
tandem up --repo /srv/project --listen 0.0.0.0:13013
# Agent 1
tandem init --tandem-server=your-vps:13013 --workspace=backend ~/work-backend
cd ~/work-backend
claude --prompt "Implement auth module in src/auth.rs. Use tandem for version control."
# Agent 2
tandem init --tandem-server=your-vps:13013 --workspace=frontend ~/work-frontend
cd ~/work-frontend
claude --prompt "Implement UI. Run tandem log to see other agents' work."
Add this to each agent's system prompt or CLAUDE.md:
You're working in a tandem workspace (jj over the network).
Use tandem instead of git for all version control:
tandem log # see all agents' commits
tandem new -m "description" # commit your changes
tandem diff -r @- # see what you changed
tandem file show -r <rev> <path> # read any agent's file
tandem bookmark create <name> -r @- # mark for review
Before starting work, run tandem log to see what others have done.
Do NOT use git commands — this repo uses tandem.
Commands#
Server lifecycle#
Start, stop, and monitor the tandem server.
tandem up --repo <path> --listen <addr> Start background daemon
tandem down Stop the daemon
tandem status Check if daemon is running
tandem logs Stream logs from daemon
tandem serve --listen <addr> --repo <path> Start server (foreground)
tandem up — starts a background daemon and returns immediately.
tandem up --repo <path> --listen <addr> [--log-level <level>] [--log-file <path>]
[--control-socket <path>]
Forks tandem serve --daemon in the background. Waits for the control socket
to become healthy, prints the PID, exits. If a daemon is already running,
exits with an error.
tandem down — stops the running daemon.
tandem down [--control-socket <path>]
Sends a shutdown request via the control socket, waits for the process to exit.
tandem status — reports whether the daemon is running.
tandem status [--json] [--control-socket <path>]
Exit code 0 = running, 1 = not running.
$ tandem status
tandem is running
PID: 1234
Uptime: 2h 15m
Repo: /srv/project
Listen: 0.0.0.0:13013
Version: 0.3.0
$ tandem status --json
{"running":true,"pid":1234,"uptime_secs":8100,"repo":"/srv/project","listen":"0.0.0.0:13013","version":"0.3.0"}
tandem logs — streams log output from the daemon.
tandem logs [--level <level>] [--json] [--control-socket <path>]
Connects to the control socket and streams log events. --level filters
server-side (trace, debug, info, warn, error). --json outputs raw JSON
lines instead of formatted text.
tandem serve — runs the server in the foreground. Use this for systemd, Docker, or debugging. Logs to stderr.
tandem serve --listen <addr> --repo <path> [--log-level <level>] [--log-format <fmt>]
[--control-socket <path>] [--log-file <path>]
Workspace setup#
tandem init --tandem-server <addr> [--workspace <name>] [path]
Initializes a tandem-backed workspace. Creates the directory, registers the
tandem backend, and connects to the server. --workspace names the workspace
(default: default). Each agent should use a unique workspace name.
Watch#
tandem watch --server <addr>
Streams head change notifications from the server. Useful for triggering rebuilds or CI when any agent commits.
Everything else#
Every jj command works through tandem:
tandem log Show commit history
tandem new -m "message" Create new change
tandem diff -r @- Show changes
tandem file show -r <rev> <path> Read file at revision
tandem bookmark create <name> -r <rev> Create bookmark
tandem describe -m "message" Update description
The tandem binary embeds jj — these are stock jj commands running against
the remote store.
Environment variables#
| Variable | Purpose |
|---|---|
TANDEM_SERVER |
Server address — fallback for --tandem-server |
TANDEM_WORKSPACE |
Workspace name (default: default) |
Why#
Coding agents need to collaborate on the same codebase without stepping on
each other. The current approach — git worktrees on a single machine — breaks
down when agents run on different machines, fight over .git locks, or need
to read each other's work-in-progress.
tandem gives each agent an isolated workspace that shares a single store over the network. Agents see each other's commits instantly. No push/pull, no merge conflicts on the transport layer. The server ships to GitHub when you're ready.
How it works#
┌──────────────┐ ┌──────────────────────────┐
│ Agent A │ Cap'n Proto RPC │ │
│ (Machine B) │◄─────────────────────────►│ tandem serve │
│ │ │ (Machine A) │
│ ~/work-a/ │ │ │
│ src/auth.rs │ │ ┌────────────────────┐ │
│ src/lib.rs │ │ │ Content-Addressed │ │
└──────────────┘ │ │ Store │ │
┌──────────────┐ │ │ │ │
│ Agent B │ Cap'n Proto RPC │ │ jj+git repo │ │
│ (Machine C) │◄─────────────────────────►│ │ operations │ │──► git push
│ │ │ │ views │ │
│ ~/work-b/ │ │ │ op heads (CAS) │ │
│ src/api.rs │ │ └────────────────────┘ │
└──────────────┘ │ │
┌──────────────┐ │ │
│ Agent C │ Cap'n Proto RPC │ │
│ (Machine D) │◄─────────────────────────►│ │
│ │ │ │
│ ~/work-c/ │ └──────────────────────────┘
│ tests/*.rs │
└──────────────┘
Each agent has a full working copy on its local disk (fast reads/writes).
The commit store lives on the server. When Agent A commits, Agent B sees it
instantly in tandem log — no fetch, no pull, no merge.
The tandem binary has two ways to run the server:
tandem up— starts a background daemon. No systemd needed.tandem serve— runs in the foreground. For systemd, Docker, debugging.
And one way to run as a client:
tandem <jj-command>— runs stock jj with tandem as the remote store.
The client registers three jj-lib trait implementations:
| Trait | What it stores | RPC calls |
|---|---|---|
Backend |
Files, trees, commits, symlinks | getObject, putObject |
OpStore |
Operations, views | getObject, putObject |
OpHeadsStore |
Operation head pointers | getHeads, updateOpHeads (CAS) |
Concurrent writes use compare-and-swap on operation heads with automatic retry. Two agents committing simultaneously both succeed — CAS contention resolves transparently.
vs git worktrees#
Most multi-agent tools (Conductor, Claude Squad, Cursor) use git worktrees for agent isolation. tandem takes a different approach:
| Git worktrees | Tandem | |
|---|---|---|
| Machine scope | Same machine only | Any machine |
| Agent visibility | Must checkout other branch | tandem log shows all instantly |
| Concurrent writes | Merge conflicts at integration | CAS convergence — both succeed |
| Store sharing | Shared .git dir (lock contention) |
Network RPC (no locks) |
| Git push | From any worktree | Server-only (single source of truth) |
| Disk usage | Full working copy × N worktrees | Full working copy × N (same) |
| Setup | git worktree add |
tandem init --workspace=<name> |
tandem trades latency (every read/write is an RPC) for cross-machine collaboration and instant visibility. If all your agents are on one machine, git worktrees are simpler. If they're on different machines, or you need agents to see each other's work without merging, tandem is what you want.
Tests#
cargo test
34 integration tests covering:
- Single-agent file round-trip (write → commit → read back exact bytes)
- Two-agent cross-workspace file visibility
- Concurrent writes from 2 and 5 agents (CAS convergence)
- Promise pipelining (rapid sequential writes)
- WatchHeads real-time notifications
- Git round-trip (tandem → jj git objects)
- End-to-end multi-agent with bookmarks
- Signal handling and graceful shutdown
- Control socket status reporting
- Daemon lifecycle (up/down)
- Log streaming
Cross-machine tested with Docker containers — see qa/v1/cross-machine-report.md.
Known limitations#
- No TLS — connections are plaintext. Use SSH tunnels or a VPN for untrusted networks.
- No auth — anyone who can reach the port can read/write the repo. Firewall the port and use SSH tunnels for access.
- Unix only for daemon management —
tandem up,tandem down,tandem status, andtandem logsuse Unix domain sockets. macOS and Linux only, not Windows. (tandem serveworks everywhere.) - No static binary yet — requires glibc 2.39+. Use matching distro or build locally.
- fsmonitor conflict — if your jj config has
fsmonitor.backend = "watchman", pass--config=fsmonitor.backend=noneto tandem commands.
Running in production#
- Back up the server repo directory — it's the source of truth.
- Git credentials on the server — the server needs SSH keys or tokens for
jj git push/jj git fetch. - Monitor disk space — all agent objects land on the server.
- Firewall the port — no auth means network-level access control is your only defense.
Project structure#
src/
main.rs CLI dispatch (clap) + jj CliRunner passthrough
server.rs Server — jj Git backend + Cap'n Proto RPC
control.rs Control socket — daemon management protocol (Unix socket, JSON lines)
backend.rs TandemBackend (jj-lib Backend trait over RPC)
op_store.rs TandemOpStore (jj-lib OpStore trait over RPC)
op_heads_store.rs TandemOpHeadsStore (CAS head management over RPC)
rpc.rs Cap'n Proto RPC client
proto_convert.rs jj protobuf ↔ Rust struct conversion
watch.rs tandem watch command
schema/
tandem.capnp Cap'n Proto schema (13 Store methods + HeadWatcher)
tests/
common/mod.rs Test harness (server spawn, HOME isolation)
slice1-7 tests Core integration tests (file round-trip, visibility, CAS, git)
slice10-13 tests Server lifecycle tests (shutdown, control socket, up/down, logs)
License#
MIT