AI writes code fast and messy, and lately in parallel. One session touches a dozen files, mixes a refactor into a feature, and takes two wrong turns before it lands, while a second agent does the same in the next terminal. Writing the code isn’t the slow part anymore. Keeping the repo straight, and turning what the agents did into changes a reviewer can follow, is.

What pushed me over was running two coding agents on the same repo. One switched branches partway through its task. The other picked that branch up, finished faster and closed it out, which left the first agent on a branch that no longer matched anything. It had to redo its work, and watching that I realized how fragile a working branch is once more than one thing moves it. In jj there’s no current branch to switch, and a bookmark moving or disappearing doesn’t take work with it: every change stays in the log under its own id.

So I switched to Jujutsu (jj). Git is built around one person on one branch making deliberate moves. jj is built around a repo that keeps changing: every edit is recorded, every change keeps its id whatever happens to the branches, and rewriting history is just how you work. It runs on top of Git, so the repo, the remote and the pull requests don’t change.

Git friction

Git has answers for all of this: interactive rebase, fixup commits with autosquash, git-absorb, worktrees, the reflog. They work. The problem is what each one costs. A rebase stops at the first conflict and blocks everything until you resolve it. Uncommitted work has to be staged or stashed before you can move, and a hard reset throws it away. A branch is the only handle on a piece of work, so when something else moves it the commits are still in there, but you’re fishing for them in the reflog.

None of that hurts when you clean up history once a week. With AI you’d do it several times a session, so it gets skipped, and what lands is one big commit nobody can really review.

The jj model

No staging area. Your working copy is a change. Every jj command saves what’s on disk, so whatever the AI just did is already recorded. Every operation is logged too, and jj undo takes back the last one, even a bad split or rebase. Letting AI try something big is safe when getting back is one command.

Changes, not branches. Every change has a stable id, and bookmarks (jj’s branches) are pointers you move when you choose to. Two agents can each work on their own change off main without touching the other’s, and jj workspace gives each its own working copy in the same repo.

Edit anything. Fix a change three steps back and everything on top rebases automatically. Conflicts don’t stop it: jj marks them and you fix them when you get there.

Reshape freely. jj split breaks a change in two, jj squash folds it into its parent, and jj absorb sends each edit back to the change that last touched those lines. Handy when AI gives you logging, a migration and a feature all at once.

Two pagination attempts

Imagine AI is adding an orders endpoint and you can’t decide on pagination.

jj git init --colocate     # once, inside an existing Git repo

jj new main -m "Orders endpoint: offset pagination"
# AI writes the endpoint, tests and some unrelated logging changes
jj split                   # move the logging edits into their own change

jj new main -m "Orders endpoint: cursor pagination"
# a second attempt, next to the first instead of on top of it
jj log                     # both attempts show up as children of main
jj abandon <change-id>     # drop the one you don't want
jj undo                    # changed your mind? the abandon is reversed

If the tests later show the interface is wrong, jj edit the change that defined it and fix it there. Everything on top follows. When it’s ready, add a bookmark (jj’s version of a branch) and push. The pull request looks like any other, just made of small changes.

Refactoring

Mitchell Hashimoto (creator of Vagrant and Terraform) says refactoring is where AI does best: “Anytime I ask it to do that, it’s always perfect.” Refactors are also what reviewers want kept apart from features. With jj you split them out afterwards instead of planning for it up front.

Rough edges

jj isn’t 1.0, and commands still change between releases, so older guides can be wrong. Git tools in the same repo keep working but see a detached HEAD, which confuses some of them. Git hooks don’t run, and submodules and LFS are still rough. Coding agents still speak Git too. In a colocated repo that mostly works, because jj notices what Git did, but tell the agent not to stash or switch branches, which is the same thing that bit me in the first place. Your team doesn’t need to switch, but you’ll be the odd one out, and habits like staging and stashing don’t carry over. Expect a few days of reaching for commands that aren’t there.

The payoff is that cleanup gets cheap enough that you actually do it, and reviewers get small changes instead of one big diff.

Takeaways

  • Writing code got fast. Making it reviewable, and keeping the repo straight with several agents in it, didn’t.
  • Git can do the cleanup. With jj split, jj squash and jj absorb it gets cheap enough that you actually do it.
  • Snapshots and jj undo make it safe to let AI try bigger things.
  • Changes keep their ids whatever happens to the bookmarks, so a moved or deleted branch strands nobody.
  • It runs on top of an existing Git repo. Your team doesn’t have to switch.

Let the AI make a mess. In jj, cleaning it up is the easy part.

Dive deeper

Jujutsu Strategies

Steve’s Jujutsu Tutorial