
One History, Many Agents: Using jj workspaces for parallel coding agents
With five coding agents, jj workspaces let me check on each one, rebase them all onto main and recover what one destroyed, without stopping any of them.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
With five coding agents, jj workspaces let me check on each one, rebase them all onto main and recover what one destroyed, without stopping any of them.
This is jj log in the main checkout of a service while three coding agents work on it, each in its own directory:
$ jj log
@ mluqyvpr you@example.com 2026-10-21 09:02:00 default@ ae140c5b
│ (empty) (no description set)
│ ○ lvnvxyzt you@example.com 2026-10-21 10:14:00 add-metrics@ 0e143d67
│ │ (no description set)
│ ○ mxxqumpk you@example.com 2026-10-21 10:05:00 16f809f2
├─╯ add metrics endpoint
│ ○ vwllwpnn you@example.com 2026-10-21 10:12:00 fix-retry@ 8ec1af22
├─╯ (no description set)
│ ○ ukspvmtq you@example.com 2026-10-21 10:08:00 add-tracing@ ef29297d
├─╯ (no description set)
◆ wtnsnvmw you@example.com 2026-10-21 09:02:00 main df085796
│ bump deps
~
fix-retry@ is one agent’s working copy. It has no description because the agent has not committed anything, and it is still a commit in my history. From my own directory, without interrupting the agent, I can see what it has changed so far:
$ jj diff --stat -r fix-retry@
README.md | 2 +-
config.toml | 2 +-
src/retry.rs | 1 +
3 files changed, 3 insertions(+), 2 deletions(-)
I run several Claude Code agents on one repo at once, each in its own terminal split. They need separate directories, or two of them edit the same file, and git has handed out directories since 2015 with git worktree add. With one agent, a worktree is all I need. With five, most of my time goes into four things that a worktree does not help with: checking on agents while they work, keeping them on the latest main as their work lands, recovering what an agent broke, and handing one agent’s work to the next. Every one of them goes through the agent’s commits and its branch, and agents commit when their instructions say so, not when I need them to. jj workspaces turn each agent’s files into a commit in one shared history, and all four become commands I run from my own checkout while the agents keep working. Every command and output here was run on jj 0.46 and git 2.56.
jj in a colocated repo, one where .jj/ sits next to .git/, registers each new workspace as a git worktree:
$ jj workspace add --name fix-retry -r 'trunk()' ../my-service-ws/fix-retry
Created Git worktree for the new workspace.
Created workspace in "../my-service-ws/fix-retry"
Inside it, git status, the IDE, gh and pre-commit hooks behave as in any worktree, so moving to workspaces costs no git tooling. What changes is where the agents’ work lives.
An agent that took a wrong turn ten minutes in is cheap to correct. One that took it an hour ago has built on it. So I look at each agent’s work while it runs, not only when it says it is done. The jj diff at the top of this post is that look.
With worktrees, an agent’s edits do not exist outside its directory until it commits. git log in my checkout shows nothing, git diff main fix-retry shows only what was committed, and the rest I can only read by going into the agent’s worktree and running git status there. Asking the agent to commit interrupts its task and leaves a half-done commit on its branch.
jj has no staging area and no commit step between a file and the history. Before any jj command runs in a workspace, jj records the directory’s files into its working-copy change, @. An agent runs jj st or jj diff every few steps, so its edits are in the commit within seconds, and the log at the top of this post is current without the agent committing anything. An edit made after the agent’s last jj command is not there yet, and jj st in its directory closes that gap.
Every change that reaches main leaves every other agent working on an old base, and the longer one goes without a rebase, the bigger its conflicts get.
With worktrees, the rebase cannot happen from my checkout, because a branch can be checked out in one worktree only:
$ git rebase main add-metrics
fatal: 'add-metrics' is already used by worktree at '~/repos/my-service-wt/add-metrics'
So it happens inside each worktree, one at a time. A conflict stops the rebase halfway, and the worktree sits in the middle of it, with the agent’s task on hold, until someone resolves it.
With workspaces, the whole round runs from my checkout. fix-retry is done, so I move main to its change and push it:
$ jj bookmark set main -r fix-retry@-
Moved 1 bookmarks to vwllwpnn 9ecc070e main* | fix retry backoff
$ jj git push --bookmark main
Changes to push to origin:
bookmark: main [move forward from df085796f3c7 to 9ecc070e22ef]
The other two agents are now one change behind, and one command moves both:
$ jj rebase -b add-metrics@ -b add-tracing@ -d main
Rebased 3 commits to destination.
New conflicts appeared in 2 commits:
lvnvxyzt 128b1a17 (conflict) (no description set)
mxxqumpk 9888b64c (conflict) add metrics endpoint
Hint: To resolve the conflicts, start by creating a commit on top of
the first conflicted commit:
jj new mxxqumpk
Then use jj resolve, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with jj diff.
Then run jj squash to move the resolution into the conflicted commit.
The rebase finished even though add-metrics changed the same line of the README as fix-retry. jj stores the conflict in the commit instead of stopping, so add-tracing is on the new main with nothing to do, and add-metrics is on it too, with the conflict recorded in its two commits for its agent to resolve.
Each workspace whose change I rewrote from outside is now stale, and the next jj command in it says so:
$ cd ~/repos/my-service-ws/add-metrics && jj st
Error: The working copy is stale (not updated since operation 64f884dcd625).
Hint: Run jj workspace update-stale to update it.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy for more information.
jj workspace update-stale in that directory checks out the rebased change, conflict markers included. I do the whole round while the agents wait for input: jj st in each workspace first, so recent edits move with the commit, then the rebase, then update-stale in each, so no agent sees the error.
I do not watch every command an agent runs. Sooner or later one runs git reset --hard or git checkout . to get out of a mess. With worktrees, git’s reflog covers commits, and git fsck --lost-found finds files that were once git added. Files the agent never added are gone.
With jj, every snapshot of the agent’s working copy is still in the repo, and jj evolog lists them:
$ cd ~/repos/my-service-ws/add-tracing && git reset --hard && git clean -fd
$ jj evolog -r add-tracing@
@ ukspvmtq you@example.com 2026-10-21 10:26:00 add-tracing@ ed794b71
│ (empty) (no description set)
│ -- operation e373b0e868da snapshot working copy
○ ukspvmtq/1 you@example.com 2026-10-21 10:22:00 6ad2264f (hidden)
│ (no description set)
│ -- operation 3fe6eb1b3b9b snapshot working copy
○ ukspvmtq/2 you@example.com 2026-10-21 10:08:00 ef29297d (hidden)
│ (no description set)
│ -- operation 973612a140dd snapshot working copy
○ ukspvmtq/3 you@example.com 2026-10-21 09:42:00 de1f63b7 (hidden)
(empty) (no description set)
-- operation 82ef971d1c45 create initial working-copy commit in workspace add-tracing
The 10:22 snapshot has the work, and restoring from it brings the files back:
$ jj restore --from 6ad2264f
Working copy (@) now at: ukspvmtq aa5de4e4 (no description set)
Parent commit (@-) : wtnsnvmw df085796 main | bump deps
Added 2 files, modified 0 files, removed 0 files
Recovery covers what jj recorded, so edits made after the agent’s last jj command are lost the same way as in git.
Undo stays inside one workspace too. Every jj command is an operation in the operation log, and jj undo reverts the last one. With several agents writing to one repo, the last operation is usually some other agent’s, and jj refuses that case:
$ jj undo
Error: Refusing to undo operation 89aefb3ab8a8 because it was performed in workspace add-metrics
Hint: Use --allow-cross-workspace to undo it anyway, or use jj op revert to revert a specific operation
An agent’s jj undo reverts its own mistake or does nothing. To take back one specific thing an agent did, jj op revert <id> reverts that operation alone.
There is no shared stash either. With worktrees, git stash is one list for the whole repo, so an agent that runs git stash pop can apply another agent’s edits to its own files. An agent in a jj workspace that wants its edits out of the way runs jj new @-: it continues on a fresh change from the same parent, and its edits stay in the old change, where only that change holds them.
Agents work well in a chain: one writes the change, a second reviews it or writes the tests, a third fixes what the review found. The second agent needs the first one’s work as the first agent left it, including edits it never committed.
With worktrees, the first agent’s branch is locked to its directory. git worktree add --detach ../review fix-retry gives the reviewer the branch’s last commit, without the uncommitted edits.
With jj, the reviewer starts on top of the first agent’s working copy:
jj workspace add --name review -r fix-retry@ ../my-service-ws/review
Its directory has every edit the first agent had made by its last jj command. This works best on a change the first agent has finished. If the first agent keeps working, each of its jj commands rewrites fix-retry@, jj rebases the reviewer’s change on top, and the reviewer’s workspace goes stale until it runs jj workspace update-stale.
Models know git much better than jj, and out of habit an agent reaches for git commit, git checkout and git stash. In a colocated repo read-only git is harmless and the writes are not, so the global CLAUDE.md that every agent of mine loads says to use jj for every write, to keep git status, git diff, git log and git blame for reading, to pass -m to every jj command that takes a message and never open an editor, and never to push.
Rules in a prompt get skipped now and then, so the ones that matter are also enforced, the way I argued for in the cargo-crap post. JJ_EDITOR=false makes a missing -m fail at once instead of leaving the agent waiting on an editor it cannot see. A Claude Code PreToolUse hook blocks git push and jj git push in a workspace, and a pre-push git hook in each workspace catches a git push from a script the hook never sees.
jj git init --colocate
jj workspace add --name fix-retry -r 'trunk()' ../my-service-ws/fix-retry
Start an agent in the new directory and run jj log from your own. jj diff -r fix-retry@ shows its work, and jj workspace remove fix-retry ends it when the change has landed. The agent’s changes stay in jj log after the directory is gone.
My dotfiles wrap these commands and fill the gaps above. jwc fix-retry creates the workspace in <repo>-ws/ next to the repo, copies the untracked .env* files from the main checkout, runs the project’s just setup or mise run setup and starts Claude Code in it. jsync fetches once and takes the workspaces one at a time: jj st in the workspace, a rebase onto trunk, update-stale straight after.
From the main checkout, jwst prints one row per workspace: how far it is behind trunk, whether it was pushed and whether it has conflicts. It runs jj only against the main checkout, never inside an agent’s directory, so looking never snapshots a running agent’s files. The repo’s parallel agents guide has the full workflow.
Software engineer who enjoys turning ideas into tangible, impactful products.
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。