⚠️ 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.
- Runs a central server that hosts a real
jj+gitrepo. - Lets each agent/machine use its own local workspace backed by that server.
- Makes all work visible across agents via normal
jjcommands (log,diff,file show,new, etc.). - Keeps shipping simple: push to GitHub from the server with
jj git push.
Published on crates.io as jj-tandem.
Requires a Rust toolchain. No system capnp binary is required for install/build.
cargo install jj-tandemOr build from source:
git clone https://github.com/laulauland/tandem.git && cd tandem
cargo build --release# On your server (VPS, or localhost for testing)
tandem up --repo ~/project --listen 0.0.0.0:13013
tandem server status
# On each agent's machine
tandem init --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.
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 server statusOn agent machines:
# Agent A
tandem init --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 --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 mainThe server is a real jj+git repo. jj git push just works.
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 --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 --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 downContainers 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 --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-netEach 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 --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 --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.
tandem status is the stock jj working-copy status command.
Use tandem server status for daemon health.
Start, stop, and monitor the tandem server.
tandem up --repo <path> [--listen <addr>] [--enable-integration-workspace]
Start background daemon
tandem down Stop the daemon
tandem server status Check if daemon is running
tandem server logs Stream logs from daemon
tandem serve --listen <addr> --repo <path> [--enable-integration-workspace]
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>]
[--enable-integration-workspace]
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.
If --listen is omitted, tandem chooses a listen address with this heuristic:
- reuse the last successful listen address for this repo (if still free),
- otherwise pick the first free port in
0.0.0.0:13013-13063, with a repo-path hash offset to reduce collisions across repos.
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 server status — reports whether the daemon is running.
tandem server status [--json] [--control-socket <path>]
Exit code 0 = running, 1 = not running.
$ tandem server status
tandem is running
PID: 1234
Uptime: 2h 15m
Repo: /srv/project
Listen: 0.0.0.0:13013
Version: 0.3.2
Integration workspace: disabled
$ tandem server status --json
{"running":true,"pid":1234,"uptime_secs":8100,"repo":"/srv/project","listen":"0.0.0.0:13013","version":"0.3.2","integration":{"enabled":false,"lastStatus":"disabled"}}
tandem server logs — streams log output from the daemon.
tandem server 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.
JSON log objects include structured fields:
ts, level, target, msg, and fields.
tandem serve — runs the server in the foreground. Use this for systemd, Docker, or debugging. Logs to stderr.
Pass --enable-integration-workspace to keep an integration bookmark updated
from active workspace heads. This mode is off by default.
tandem serve --listen <addr> --repo <path> [--log-level <level>] [--log-format <fmt>]
[--control-socket <path>] [--log-file <path>]
[--enable-integration-workspace]
tandem init --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.
If omitted, tandem auto-generates a unique workspace name to avoid cross-device
workspace collisions by default.
tandem watch --server <addr>
Streams head change notifications from the server. Useful for triggering rebuilds or CI when any agent commits.
Every jj command works through tandem:
tandem status Show working-copy status
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.
| Variable | Purpose |
|---|---|
TANDEM_SERVER |
Server address — fallback for --server |
TANDEM_WORKSPACE |
Workspace name fallback for tandem init when --workspace is not provided. |
TANDEM_LISTEN |
Listen address fallback for tandem up --listen. |
TANDEM_ENABLE_INTEGRATION_WORKSPACE |
Set to 1/true to enable integration workspace mode when --enable-integration-workspace is not passed. |
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.
┌──────────────┐ ┌──────────────────────────┐
│ 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 | getOperation, putOperation, getView, putView, resolveOperationIdPrefix |
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.
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.
cargo test38 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)
- Cap'n Proto transport correctness under rapid sequential writes (slice 4)
- 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
Performance evidence (latest local benchmark run):
- Commit-path latency under injected RTT now has machine-readable baseline vs optimized data in:
docs/benchmarks/tcp_commit_path_latest.json
- In-flight throughput under concurrent writers is tracked in:
docs/benchmarks/tcp_inflight_throughput_latest.json
- Current status: latest artifacts show modest gains under injected RTT/contention (p95 +3.33%/+4.68% on P1/P2, throughput geometric mean ~1.001x); earlier stretch targets (p95 >=20%, throughput >=1.5x) are deferred follow-on optimization work.
Cross-machine tested with Docker containers — see qa/v1/cross-machine-report.md.
- 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.
- Raw TCP transport only (today) — store RPC currently runs over Cap'n Proto on TCP. In sandboxed VM environments that restrict outbound traffic to HTTP(S)/WebSocket or SSH exec only, you may need tunneling. Planned transport expansion is documented in
docs/design-docs/transport-matrix.md. - Unix only for daemon management —
tandem up,tandem down,tandem server status, andtandem server 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.
- 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.
tandem checks in generated bindings at src/tandem_capnp.rs.
When you change schema/tandem.capnp, regenerate via:
TANDEM_REGENERATE_BINDINGS=1 cargo build(build.rs compiles from schema when capnp is available, and falls back to
checked-in bindings otherwise.)
src/
main.rs CLI dispatch (clap) + jj CliRunner passthrough
tandem_capnp.rs Generated Cap'n Proto bindings (checked in)
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)
build.rs Build-time schema generation with checked-in fallback
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)
MIT