Jiu-Jitsu Con is in Las Vegas this week — the IBJJF World Masters, thousands of grapplers, @jiujitsucon and @ibjjf all over your feed. We build a different kind of jiu-jitsu: Jujutsu, the version control system, and this week we’re shipping two projects built on it. No gis were harmed in the making of this post. Let’s get technical.
What Jujutsu (jj) actually is
If you’ve only used git, here’s jj in three sentences. There’s no staging area — your working directory is always a commit, so add and the “should I commit this half-finished mess” dance both disappear. Every change gets a stable change ID that survives rebases, squashes, and amends, so a change is a change is a change no matter how many times its underlying commit hash moves. And every operation — not just commits, everything — is recorded in an append-only operation log, so jj undo reverses anything, including the rebase you regretted three commands ago. jj is Google-originated and community-supported (that community lives at the jj Discord and GitHub Discussions); it speaks Git under the hood, so nothing about your Git remotes has to change.
We didn’t build jj. We built two things on top of it.
isomorphic-jj: jj semantics in plain JavaScript
@johnhenry/isomorphic-jj (docs) is a from-scratch, pure-JavaScript implementation of jj’s model — not a wrapper around the Rust binary. It runs in Node and in the browser, using isomorphic-git for the Git backend and IndexedDB or OPFS for storage when there’s no filesystem to talk to. Stable change IDs, the working-copy-is-a-commit model, a full operation log with undo, conflicts represented as data instead of merge markers — all of it, in JS, MIT licensed, 1730 tests.
Here’s the entire core loop, trimmed from the repo’s own examples (examples/01-init-commit-log.mjs) — it runs as-is:
import fs from 'node:fs';
import { createJJ } from '@johnhenry/isomorphic-jj';
const jj = await createJJ({ fs, dir: './repo' });
await jj.init({ userName: 'Alice', userEmail: 'alice@example.com' });
await jj.write({ path: 'README.md', data: '# hello\n' });
await jj.describe({ message: 'Initial commit' });
await jj.new({ message: 'Add a feature' });
await jj.write({ path: 'feature.js', data: 'export const feature = 1;\n' });
await jj.undo(); // changed your mind? undo the last operation
No git add. No staging index to reason about. describe() names the change you’re on; new() finalizes it and starts the next one; undo() reverses whatever you just did, not just commits — it’s the same operation log backing every command.
What that unlocks: version control that doesn’t need a server or a filesystem. An in-browser editor can give every document its own repo, with full history and undo, stored in IndexedDB, no backend. An offline-first app can snapshot state locally and reconcile later over Git remotes when connectivity returns. And because each createJJ() instance is just a JS object over a pluggable backend, it’s a clean sandbox for a coding agent — spin up an isolated, disposable repo in memory, let the agent work, undo anything that goes sideways, and throw the whole thing away when you’re done. Twelve runnable examples in the repo (examples/) walk through stacked changes, conflicts, revsets, Git interop, and more — each one is self-contained, asserts what it demonstrates, and doubles as a smoke test in CI.
It’s 0.2.0 as of today, renamed from an unscoped isomorphic-jj package that had about ten months of releases before moving into the @johnhenry npm scope. Same lineage, new address.
JJHub: the same model, projected onto GitHub
Here’s the problem isomorphic-jj doesn’t solve on its own: the moment your work hits GitHub, jj’s model flattens back into branches and force-pushes. PR-per-commit means every amend renames a branch or force-pushes it, every reviewer restarts from a diff they’ve half-seen before, and there’s no undo for the platform itself — just git reflog archaeology, per-repo, per-ref.
JJHub is an overlay that puts jj’s model back on top of plain GitHub — no migration, no new host. A change gets a stable ID (CHG-184) that’s embedded in the PR body and survives every rewrite. Stacks of dependent changes become stacked PRs, cross-linked with “part N of M” footers, that land root-to-tip in one CI-gated action. And because every mutation across changes, stacks, bookmarks, and conflicts goes through one operation log, jjhub undo reverses platform-level mistakes, not just local ones — with a guardrail that refuses to lie about merges that already happened on GitHub. Under the hood, the sync daemon runs on @johnhenry/isomorphic-jj itself, which is how it drives a jj working copy without requiring the Rust jj binary on your machine.
The part we think is most interesting for this crowd: JJHub speaks MCP. jjhub mcp serve exposes the change/stack/land/undo model as structured tools a coding agent can call directly, instead of handing it a raw shell and hoping. Every action is attributed in the operation log; when an agent reaches for something destructive — a force-land, a trunk delete — the action doesn’t fail open and doesn’t fail silently. It parks as a durable confirmation that any authorized human approves or denies later, optionally signed with the SSH or GPG key already registered on their GitHub account.
A five-command session, taken straight from the getting-started guide:
npm install -g jjhub
jjhub auth login --url https://jjhub.erisera.com
jjhub repo init owner/name
jjhub change new "Add a feature" --repo owner/name
jjhub change list --repo owner/name
That last command prints the change ID you’ll see mirrored as a PR on github.com — your CI, your review flow, your host, untouched.
Try it
- isomorphic-jj: github.com/johnhenry/isomorphic-jj,
npm install @johnhenry/isomorphic-jj, docs at opensource.johnhenry.me/isomorphic-jj - JJHub: jjhub.erisera.com,
npm install -g jjhub, MCP endpoint athttps://jjhub.erisera.com/mcp
Where this is today
Both are early, and we’d rather say so than let a launch post oversell it. isomorphic-jj is at 0.1.x/0.2.0: broad JJ CLI parity (tracking real jj through v0.44), a large test suite, and working browser support — but it’s a from-scratch reimplementation of jj’s semantics, not the Rust binary, so edge cases in revset evaluation or conflict handling can and do diverge; the project calls itself “ready for experimentation and prototyping,” not a drop-in replacement for the real thing. JJHub’s core is built and tested end-to-end against both a mock GitHub gateway and a fake jj client, and the real-GitHub path has been live-verified — but the jj log template hasn’t yet been checked against a real jj binary in production use, and the merge queue is per-repo serialized landing semantics on a single instance, not a standalone durable queue service. Neither of these is finished software. Both are usable today, and we’re building them in the open.
If you’re at Jiu-Jitsu Con this week, we hope your submissions are cleaner than our merge conflicts. If you’re not, there’s still a working copy waiting for you either way.