Why jj for Version Control
Stacked changes and first-class rebases for multi-agent workflows
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:
Rebasing is scary. In Git, rebasing can lose work. Developers avoid it, leading to merge commit spaghetti.
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.
Branches proliferate. Every feature, every agent task, every experiment—another branch. Managing them becomes overhead.
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
| Option | Pros | Cons |
|---|---|---|
| GitThe industry standard. Distributed, ubiquitous, complex. |
|
|
| jj (Jujutsu)Git-compatible VCS with first-class rebasing and stacked changes. |
|
|
| Sapling (Meta)Git-compatible VCS focused on stacked diffs. |
|
|
Future Outlook
Git will remain the protocol. But the client will evolve.
jj represents the future of local version control:
Change-centric, not branch-centric. You work on changes, not branches. Branches are just labels that follow automatically.
Stacked changes as the default. Building on unmerged work is natural. When upstream changes, your stack rebases automatically.
Conflicts are data, not blockers. jj stores conflicted files and lets you resolve later. You can commit, rebase, and share conflicted work.
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:
- Create branch A for feature
- Agent implements, submits PR
- While A in review, create branch B from main
- A gets feedback, changes
- B now needs changes from A—manual rebase
- Conflicts. Resolve manually.
- Repeat for every dependent feature.
With jj and stacked changes:
- Create change A for feature
- Agent implements, push for review
- Create change B on top of A
- A gets feedback, amend change A
- B automatically rebases—no manual work
- Conflicts? Stored in B, resolve when ready
- 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
One change per spec slice. Each change maps to one piece of the specification. Small, focused, reviewable.
Keep slices small. Target ~300 lines when possible. Smaller changes review faster, conflict less, merge easier.
Rebase from trunk frequently. Don't let your stack drift.
jj rebase -d mainoften.Use stacked PRs for larger features. Feature = stack of 3-5 changes, each reviewable independently. Merge bottom-up.
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:
- Use jj locally,
jj git pushto GitHub - Teammates use Git directly
- CI uses Git
- Gradually adopt jj as people see benefits
No big-bang migration required.
Spec-Driven Development + jj
The combination is powerful:
- Branch/change per spec:
jj new -m "Implement UserService" - Gate before implementation: Spec reviewed, then implementation starts
- Small slices: Each test scenario can be its own change
- Stack naturally: Implementation builds on interface, tests build on implementation
- Rebase often:
jj rebase -d mainkeeps stack fresh
The result: clean history, parallel work, automatic rebasing, fewer conflicts.
Examples
# 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 updatedStacked changes workflow. Each change builds on the previous. When a lower change updates, the stack automatically rebases. Push each for independent review.
# 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 resolvedjj stores conflicts in the commit itself. You can continue working, rebasing, even pushing conflicted changes. Resolve when convenient.
## 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 changesAdd this section to AGENTS.md when using jj for multi-agent workflows. Establishes clear rules for version control behavior.