Originally published on 2026-01-06
Original article (Japanese): jj workspace: コンフリクトで止まらないvibe coding並列開発
Recently, while developing with Claude Code running in parallel across four instances, I encountered a bit of a problem.
I created four directories using git worktree and assigned the AI to implement features in each one—up to this point, everything was smooth. However, when it came time to merge, I was hit with a storm of "CONFLICT" messages. If one worktree gets blocked by a conflict, all the work derived from it gets blocked as well, ultimately resulting in the AI just sitting idle.
"I don’t want to stop working just because a conflict occurred..." I thought, and while researching, I discovered a version control system called Jujutsu (jj), developed primarily by Google engineers. This tool turned out to be quite effective and fit my vibe coding style perfectly, so I wanted to share it.
What is Jujutsu (jj)?
Jujutsu is an open-source VCS that is compatible with Git, initiated by engineers at Google. Although it is not an official Google product, it is still actively developed by Googlers. The key point is its "Git compatibility," allowing it to be integrated directly into existing Git repositories.
So, what are the benefits?
- Work does not stop even if conflicts occur: This is the biggest advantage. Conflicts are recorded in commits, allowing other work to continue normally.
-
All operations are logged: You can view the entire history with
jj op log, and you can revert to any point. This is subtly helpful when the AI's output is not quite right. -
Automatic commits: Files are committed as soon as they are saved. No need for
git addor similar commands. - Built with Rust and fast: This is just a bonus.
Installation
If you're on macOS, you can install it with brew install jj.
# macOS (Homebrew)
brew install jj
# Linux (cargo)
cargo install --git https://github.com/jj-vcs/jj jj-cli
# Check version
jj --version
Conceptual Differences from Git
Initially, I was a bit confused by the subtle conceptual differences from Git. Once you get used to it, it becomes easier, but at first, I was like, "Wait, there are no branches?"
| Concept | Git | Jujutsu (jj) |
|---|---|---|
| Working Copy | Managed in the staging area (index) | Always one commit (@) |
| Branches | Required (detached HEAD is a special state) | Not needed (can work anonymously) |
| Conflicts | Treated as errors (blocks work) | Recorded in commits (work can continue) |
| History Manipulation | rebase (risky) | Automatic rebase (safe) |
The especially "no need for branches" aspect felt odd at first, but in vibe coding, it allows you to "start working and decide on a name later," which turned out to be quite convenient.
Basic Operations in jj
Trying out jj in an existing Git repository is straightforward. It can be used alongside Git, so if you don’t like it, you can revert back.
Initializing a Repository
# Initialize jj in an existing Git repository
cd your-git-repo
jj git init --git-repo .
# Check the current state
jj log
Example output of jj log:
```text {hl_lines="1"}
@ qpvuntsm tumf@example.com 2026-01-05 14:30:00
│ (empty) (no description set)
◉ rlvkpntz tumf@example.com 2026-01-05 14:25:00 main
│ Add README
~
`@` represents the current working copy commit. It is similar to Git's `HEAD`, but it always treats uncommitted changes as part of a commit.
### Basic Change Flow
If you're used to Git, you might initially think, "Wait, what?" since you don’t need `git add` or `git commit`.
```bash
# 1. Start a new change
jj new main -m "Add feature A"
# 2. Edit files
vim src/feature.js
# 3. Check the state (already committed!)
jj log
# 4. Add/modify description
jj describe -m "Add authentication feature"
# 5. Start the next change
jj new -m "Add tests for feature A"
As soon as you save the file, it’s already committed, eliminating the need for the git add → git commit flow. Honestly, this alone makes it much more comfortable.
Editing Commits
Editing past commits is also easy. In Git, I would nervously use git rebase -i, but with jj, it’s much more relaxed.
# Edit a past commit
jj edit <change-id>
vim src/feature.js
jj describe -m "Updated feature"
# Return to the original position
jj edit @
Moreover, descendant commits are automatically rebased, so you don’t have to worry about dependencies.
git worktree vs jj workspace
Now, onto the main topic. To engage in vibe coding with parallel development, you need "multiple working directories."
Git has git worktree, while jj has jj workspace. While they may seem similar at first glance, there are several critical differences.
Basic Mechanism is Similar
The directory structure looks similar:
repo/ # Main directory
├── .git/ # Git data (shared)
├── .jj/ # jj data (shared)
└── src/
workspace-1/ # Parallel working directory 1
└── src/
workspace-2/ # Parallel working directory 2
└── src/
You will still need to run npm install in each directory for both, so there's no escaping that.
Three Key Differences
Now, here are the important differences that made me realize, "Oh, these are completely different."
Difference 1: Behavior During Conflicts (This is the Biggest One)
With git worktree, it goes like this:
# Merge feature-A in worktree-1
cd ../worktree-1
git merge feature-A # ✅ Success
# Merge feature-B in worktree-2
cd ../worktree-2
git merge feature-B # ❌ CONFLICT!
# At this point, worktree-2 is blocked
# Even if you create a new branch in another worktree, it branches from the main before the conflict
When worktree-2 gets blocked, all work derived from it also stops. This was painful.
On the other hand, with jj workspace:
# Rebase feature-A in workspace-1
cd ../workspace-1
jj rebase -s feature-A -d main # ✅ Success
# Rebase feature-B in workspace-2
cd ../workspace-2
jj rebase -s feature-B -d feature-A # ⚠️ Conflict occurs
# But work can continue!
jj log
# @ feature-B (conflict) ← Conflict state is recorded
# ◉ feature-A
# ◉ main
# Start a different task in workspace-3
cd ../workspace-3
jj new main -m "feature-C" # ✅ Work starts without issues
Conflicts are "recorded" only, and other work is not blocked. This is incredibly helpful for vibe coding.
{{< figure-desc src="/images/jj-workspace-vibe-coding-parallel-development/conflict-comparison-diagram.png" alt="Comparison diagram of conflict handling between git worktree and jj workspace" caption="git worktree blocks on conflict, while jj records it and allows continuation" >}}
Difference 2: State Sharing and Visibility
In git worktree, uncommitted changes in each worktree are not visible from others:
# Changes in worktree-1 (uncommitted)
cd ../worktree-1
echo "test" > newfile.txt
# Not visible from worktree-2
cd ../worktree-2
ls newfile.txt # ❌ Does not exist
In jj, changes are automatically committed, so they are visible from all workspaces:
# Changes in workspace-1 (auto-committed)
cd ../workspace-1
echo "test" > newfile.txt
# Visible from workspace-2
cd ../workspace-2
jj log # Changes from workspace-1 are displayed with the @ mark
This reduces the instances of "Wait, where was I working and what was I doing?"
Difference 3: Automatic Rebase for Dependent Branches
This is also subtly convenient. When there are dependencies like feature-A → feature-B → feature-C.
In git worktree, if you fix feature-A, you need to manually rebase B and C:
cd ../worktree-A
git commit --amend -m "Fix feature A"
# B and C need manual rebase
cd ../worktree-B
git rebase feature-A # Manual
cd ../worktree-C
git rebase feature-B # Manual
With jj, it does it automatically for you:
cd ../workspace-A
jj describe -m "Fix feature A"
# B and C are automatically rebased
jj log # All updated
Since I often fine-tune code generated by AI later, this is a nice feature.
Practical Workflow for Vibe Coding
Here’s how I actually run my workflow with Claude Code in four parallel instances.
{{< figure-desc src="/images/jj-workspace-vibe-coding-parallel-development/workspace-architecture-diagram.png" alt="Architecture diagram of parallel development with jj workspace" caption="Four workspaces operate in parallel around a shared repository hub" >}}
Setup
The initial setup looks like this. It’s a bit tedious to run npm install four times, but it’s just the first time, so it’s manageable.
# Initialize in the project directory
cd my-project
jj git init --git-repo .
# Create four workspaces
jj workspace add ../workspace-1
jj workspace add ../workspace-2
jj workspace add ../workspace-3
jj workspace add ../workspace-4
# Install dependencies in each workspace (this is unavoidable)
for i in {1..4}; do
cd ../workspace-$i
npm install
done
Starting Parallel Tasks
I throw different tasks at the AI in each workspace. I open four terminal windows and run Claude Code in each.
# workspace-1: Authentication feature
cd ../workspace-1
jj new main -m "Add authentication"
# Ask Claude Code to "Implement JWT authentication"
# workspace-2: Database optimization
cd ../workspace-2
jj new main -m "Optimize database queries"
# Ask Claude Code to "Fix N+1 queries"
# workspace-3: Adding UI components
cd ../workspace-3
jj new main -m "Add user profile component"
# Ask Claude Code to "Create a profile screen"
# workspace-4: Adding tests
cd ../workspace-4
jj new main -m "Add integration tests"
# Ask Claude Code to "Write E2E tests"
While the AI is working, I switch to another workspace to review or think about the next task.
Handling Conflicts
When a conflict actually occurs, for example, if workspace-1 and workspace-2 edit the same file:
# Integrate changes from workspace-1 into main
cd ../workspace-1
jj rebase -s @ -d main # ✅ Success
# When trying to integrate changes from workspace-2...
cd ../workspace-2
jj rebase -s @ -d main # ⚠️ Conflict occurs
# But work can continue
jj log
# @ workspace-2-change (conflict)
# ◉ workspace-1-change
# ◉ main
# Work in workspaces 3 and 4 is unaffected!
cd ../workspace-3
jj new main -m "Continue other work" # ✅ No issues
The key point is that "conflict resolution can be done later in bulk." This prevents the AI from sitting idle.
# After all tasks are complete, resolve conflicts in bulk
cd ../workspace-2
jj edit @ # Move to the commit with conflicts
jj resolve # Resolve interactively
jj describe -m "Optimize queries (resolved conflict)"
Final Merge
Once everything is done, I push everything at once. Pushing to GitHub is done in the usual way.
# Check changes in all workspaces
cd my-project
jj log --all
# Organize with squash if necessary
jj squash -s <change-id> -d main
# Push as a Git repository
jj git push
Tracking Prompt History with Operation Log
This is a side benefit I discovered, but the operation log is handy for tracking "what was generated with which prompt."
{{< figure-desc src="/images/jj-workspace-vibe-coding-parallel-development/operation-log-diagram.png" alt="Timeline diagram of the operation log" caption="All operations are recorded, allowing restoration to any point" >}}
Record of All Operations
# Display all operations
jj op log
# Example output
@ qpvuntsm agent-1@example.com 2026-01-05 15:30:00 op_abc123
│ describe "Implement authentication"
◉ sqpuoqvx agent-2@example.com 2026-01-05 15:29:45 op_def456
│ edit src/auth.js
◉ rqxostpw agent-1@example.com 2026-01-05 15:29:30 op_ghi789
new --after main
Record Prompts in Commit Messages
I tend to include the prompts I give to the AI directly in the commit messages:
jj describe -m "Prompt: Add user authentication with JWT.
Implementation includes:
- Login endpoint with email/password
- Token generation and validation
- Middleware for protected routes"
This way, if I later wonder, "Why is this code like this?" I can refer back to the prompt, which is very convenient.
Restoring to Specific Operations
If I want to roll back because "the AI's output was not quite right," I can easily revert:
# Return to a past operational state
jj op restore op_abc123
# Return to the current state
jj op restore @
In Git, you would struggle with git reflog, but jj is more intuitive.
Naming Branches Later with Bookmarks
This feature also suits vibe coding well. You can "start working and decide on a name later."
# Start working anonymously (no branch name needed)
jj new main
# Implement with Claude Code...
# Name it after completion
jj bookmark create user-auth-feature
# Push for GitHub PR
jj git push --bookmark user-auth-feature
In Git, you have to decide on a branch name upfront with git checkout -b feature-xxx, but when having the AI implement, you often don’t know what you’ll end up with until the end. jj eliminates that issue.
Should You Continue Using jj?
Honestly, it’s a bit tricky to recommend it to everyone.
Who jj is Suitable For
- Developers using AI agents in parallel
- Environments where conflicts frequently occur (often editing the same files)
- Exploratory development involving trial and error
- Frequent history shaping and editing
Who Should Stick with git worktree
- Teams where everyone is familiar with Git (to avoid learning costs)
- Those who prioritize IDE Git integration (jj's IDE support is still developing)
- Environments where conflicts rarely occur
- Those who only need 1-2 parallel instances
Conclusion
I was stuck with the "everything stops due to conflicts" issue while doing parallel development with git worktree, and switching to jj resolved that.
To summarize the advantages of jj:
- Conflicts do not block work: They are recorded, allowing work to continue.
- Visibility of state: All changes are visible from all workspaces.
- Automatic rebase: Changes in dependencies are automatically propagated.
There is a learning curve, but for those who want to avoid letting the AI sit idle during vibe coding, it’s worth trying. Since it’s Git-compatible, you can always revert if you don’t like it.
Start by trying jj git init --git-repo . in an existing project.
Top comments (0)