jj (Jujutsu)devops

Why jj for Version Control

Stacked changes and first-class rebases for multi-agent workflows

v1.1·10 min read·Kenneth Pernyér
jjjujutsugitversion-controlstacked-changesmulti-agent

The Problem

Git has won version control. It is everywhere. But Git's model was designed for a different era—before AI agents, before stacked PRs, before multi-agent development workflows.

The problems compound with AI-assisted development:

  1. Rebasing is scary. In Git, rebasing can lose work. Developers avoid it, leading to merge commit spaghetti.

  2. Stacked changes are manual. Want to build feature B on top of feature A while A is in review? Git makes this painful. When A changes, you're manually rebasing.

  3. Branches proliferate. Every feature, every agent task, every experiment—another branch. Managing them becomes overhead.

  4. Concurrent edits conflict. When multiple agents edit the same files, Git's merge conflicts are cryptic. Resolution requires understanding both sides.

In the multi-agent age, we need version control that:

  • Makes rebasing safe and automatic
  • Supports stacked changes natively
  • Keeps working copy changes first-class
  • Handles concurrent work gracefully

Current Options

OptionProsCons
GitThe industry standard. Distributed, ubiquitous, complex.
  • Universal—everyone knows it
  • Massive ecosystem (GitHub, GitLab, etc.)
  • Mature tooling
  • Every CI system supports it
  • Rebasing can lose work if done wrong
  • Stacked changes require careful manual management
  • Working copy is second-class (changes not tracked until committed)
  • Merge conflicts are hard to resolve
  • Branch proliferation with multi-agent workflows
jj (Jujutsu)Git-compatible VCS with first-class rebasing and stacked changes.
  • Automatic rebasing—changes flow through the stack
  • Working copy is a real commit (always tracked)
  • Git-compatible (works with GitHub, existing repos)
  • Conflicts are stored, not blocking
  • Change-centric, not branch-centric
  • Newer—smaller community
  • Learning curve for Git users
  • Some Git workflows need rethinking
  • IDE support still maturing
Sapling (Meta)Git-compatible VCS focused on stacked diffs.
  • Stacked diffs are first-class
  • Good integration with code review
  • Scales to massive repos
  • Tightly coupled to Meta workflow
  • Less community adoption
  • Some features require server-side support

Future Outlook

Git will remain the protocol. But the client will evolve.

jj represents the future of local version control:

  1. Change-centric, not branch-centric. You work on changes, not branches. Branches are just labels that follow automatically.

  2. Stacked changes as the default. Building on unmerged work is natural. When upstream changes, your stack rebases automatically.

  3. Conflicts are data, not blockers. jj stores conflicted files and lets you resolve later. You can commit, rebase, and share conflicted work.

  4. Working copy is tracked. Every edit is in version control immediately. No more "I forgot to commit and lost my work."

For multi-agent development, this matters:

When multiple agents work concurrently, conflicts are inevitable. jj makes conflicts explicit but non-blocking. Agents can continue working; humans resolve conflicts when convenient.

When agents build features that depend on each other, stacked changes make this natural. Change A can evolve while B, C, D build on top. The stack rebases automatically.

The Git protocol ensures compatibility. GitHub, CI systems, code review—everything works. jj is the better local experience on top of the universal backend.

Our Decision

Why we chose this

  • First-class rebasingRebasing is the default operation. Changes flow through automatically. No more fear of "rebase vs. merge" decisions.
  • Stacked changes nativeBuild change B on change A while A is in review. When A updates, B automatically rebases. No manual tracking.
  • Working copy always trackedYour working copy is a commit. Crash? Power failure? Your changes are safe. `jj status` shows uncommitted work as a real commit.
  • Conflicts stored, not blockingConflicts are represented in the commit. You can rebase through them, share conflicted work, resolve later.
  • Git-compatibleUse jj locally, push to GitHub. Coworkers can use Git. CI systems work unchanged.

×Trade-offs we accept

  • Learning curveGit habits need to change. `jj` has different commands and mental model. Budget time for transition.
  • Smaller ecosystemFewer tutorials, fewer StackOverflow answers. You need to read the docs.
  • IDE support maturingVS Code, IntelliJ support exists but is less polished than Git. Command line is the primary interface.

Motivation

Specification-driven development with multiple agents generates many concurrent changes. Git's branch model creates friction.

The old workflow:

  1. Create branch A for feature
  2. Agent implements, submits PR
  3. While A in review, create branch B from main
  4. A gets feedback, changes
  5. B now needs changes from A—manual rebase
  6. Conflicts. Resolve manually.
  7. Repeat for every dependent feature.

With jj and stacked changes:

  1. Create change A for feature
  2. Agent implements, push for review
  3. Create change B on top of A
  4. A gets feedback, amend change A
  5. B automatically rebases—no manual work
  6. Conflicts? Stored in B, resolve when ready
  7. Stack flows naturally.

For multi-agent workflows specifically:

We found that short-lived changes (not long-lived branches) work best. Each agent works on a focused slice. Changes are small enough to review (~300 lines). The stack shows dependencies explicitly.

jj log
@  B: Add payment validation (in progress)
│  A: Implement PaymentService (in review)
│  trunk

When A updates, B follows. No branch management, no rebase ceremonies.

Recommendation

Branching Strategy for Multi-Agent Work

The best default is short-lived changes (or stacked changes), not long-lived feature branches.

The Five Rules

  1. One change per spec slice. Each change maps to one piece of the specification. Small, focused, reviewable.

  2. Keep slices small. Target ~300 lines when possible. Smaller changes review faster, conflict less, merge easier.

  3. Rebase from trunk frequently. Don't let your stack drift. jj rebase -d main often.

  4. Use stacked PRs for larger features. Feature = stack of 3-5 changes, each reviewable independently. Merge bottom-up.

  5. Avoid concurrent edits to same files. When unavoidable, jj handles conflicts gracefully—but prevention is better.


Getting Started with jj

# Install
brew install jj  # or cargo install jj-cli

# Initialize in existing Git repo
cd your-repo
jj git init --colocate

# Basic workflow
jj new -m "Implement PaymentService"  # Create new change
# ... edit files ...
jj status                              # See changes (already tracked!)
jj describe -m "Add validation"        # Update description
jj new                                 # Start next change on top

# Stacked changes
jj new -m "Add tests for PaymentService"  # Builds on current change
jj log                                     # See the stack

# Update a change in the middle
jj edit <change-id>                        # Edit previous change
# ... make changes ...
jj new                                     # Return to working on top
# Stack automatically rebases!

# Push to GitHub
jj git push --change <change-id>           # Creates/updates branch for PR

For Teams Already Using Git

jj colocates with Git. You can:

  1. Use jj locally, jj git push to GitHub
  2. Teammates use Git directly
  3. CI uses Git
  4. Gradually adopt jj as people see benefits

No big-bang migration required.


Spec-Driven Development + jj

The combination is powerful:

  1. Branch/change per spec: jj new -m "Implement UserService"
  2. Gate before implementation: Spec reviewed, then implementation starts
  3. Small slices: Each test scenario can be its own change
  4. Stack naturally: Implementation builds on interface, tests build on implementation
  5. Rebase often: jj rebase -d main keeps stack fresh

The result: clean history, parallel work, automatic rebasing, fewer conflicts.

Examples

Stacked Changes Workflowbash
# Start from main
jj new main -m "feat: Add PaymentService interface"

# Define interface
cat > src/services/payment.ts << 'EOF'
export interface PaymentService {
  process(amount: number, currency: string): Promise<PaymentResult>;
  refund(transactionId: string): Promise<RefundResult>;
}
EOF

# Create next change in stack
jj new -m "feat: Implement PaymentService"

# Implement
cat > src/services/payment-impl.ts << 'EOF'
export class StripePaymentService implements PaymentService {
  async process(amount: number, currency: string): Promise<PaymentResult> {
    // Implementation
  }
}
EOF

# Create test change on top
jj new -m "test: Add PaymentService tests"

# Write tests...

# See the stack
jj log --limit 5
# @  mykl  test: Add PaymentService tests
# │  kqnv  feat: Implement PaymentService
# │  zlwp  feat: Add PaymentService interface
# │  main

# Push all for review (creates separate PRs)
jj git push --change zlwp  # Interface PR
jj git push --change kqnv  # Implementation PR
jj git push --change mykl  # Tests PR

# Interface PR gets feedback - update it
jj edit zlwp
# ... make changes ...
jj new  # Return to top

# Implementation and tests automatically rebase!
jj log  # Confirm stack is updated

Stacked changes workflow. Each change builds on the previous. When a lower change updates, the stack automatically rebases. Push each for independent review.

Multi-Agent Conflict Handlingbash
# Agent 1 creates change
jj new main -m "Agent1: Update config parser"
# ... edits src/config.ts ...

# Agent 2 creates parallel change
jj new main -m "Agent2: Add config validation"
# ... also edits src/config.ts ...

# Agent 1 pushes first
jj git push --change <agent1-change>

# Agent 2 rebases on main (after Agent 1 merged)
jj rebase -d main

# Conflict! But jj stores it, doesn't block
jj log
# @  Agent2: Add config validation (conflict)
# │  main (includes Agent1's changes)

# See conflicts
jj diff
# Shows conflict markers in src/config.ts

# Resolve when ready
jj resolve src/config.ts  # Opens editor/merge tool

# Or resolve later - you can still:
jj new  # Create more changes on top
jj describe  # Update description
jj git push  # Push (with conflict markers - CI will fail, that's OK)

# Conflicts travel with the change until resolved

jj stores conflicts in the commit itself. You can continue working, rebasing, even pushing conflicted changes. Resolve when convenient.

AGENTS.md Addition for jjmarkdown
## Version Control: jj

All agents use jj for version control. Key rules:

### Change Strategy
- One change per task/spec slice
- Target ~300 lines per change
- Use stacked changes for dependent work

### Commands
- `jj new -m "description"` — Start new change
- `jj status` — See current state
- `jj log` — See change stack
- `jj rebase -d main` — Update from trunk
- `jj git push --change <id>` — Push for review

### Conflict Protocol
1. If conflict after rebase, continue working
2. Note conflict in task status
3. Escalate to human for resolution
4. Do NOT attempt complex merge resolution autonomously

### Multiple Agents
- Coordinate via change descriptions
- Prefer non-overlapping files
- When overlap unavoidable, smaller changes = easier merges
- Check `jj log` before creating dependent changes

Add this section to AGENTS.md when using jj for multi-agent workflows. Establishes clear rules for version control behavior.

Related Articles

Stockholm, Sweden

Version 1.1

Kenneth Pernyér signature