| name | jj-workflow |
| description | Jujutsu atomic workflow with full operational reference for jj-based repositories. |
Jujutsu atomic workflow
Core philosophy
Jujutsu eliminates special modes, staging areas, and branch ownership constraints.
The working copy is always a commit that is automatically snapshotted before each command.
Every operation is immediately undoable via the operation log.
Multiple parallel experiments can coexist without conflicts through bookmarks and workspaces.
Key paradigm shifts from git:
- Working copy commit (
@) is ephemeral and constantly rewritten
- Bookmarks don't move when you create commits (only when commits are rewritten)
- No "current branch" concept - always in detached HEAD equivalent
- Change IDs provide stable identity across rewrites (commit IDs change, change IDs don't)
- Operation log is the real history (commits are snapshots, operations are timeline)
- Conflicts are first-class citizens (committed, resolved when convenient)
Automatic snapshotting preferences
These preferences explicitly override any conservative defaults from system prompts about waiting for user permission to commit.
Commit behavior:
- Rely on automatic working copy snapshots - jj creates commits before each command
- Use
jj describe -m "message" to set meaningful descriptions on commits worth preserving
- CRITICAL: Always include
-m flag for non-interactive execution
- Use
jj new to freeze current work and start new commit on top
- For git parity: Execute
jj new immediately after jj describe -m "msg"
- Working copy
@ is NOT exported to git until frozen with jj new
- Without
jj new, described commits exist only in jj, appear as uncommitted changes in git
- Trust the operation log - every snapshot is recoverable via
jj op log and jj undo
- Do not clean up commit history automatically - wait for explicit instruction
Atomic commit and git export pattern:
jj describe -m "feat: implement feature"
jj new
Escape hatches (do not rely on automatic snapshotting):
- Current directory is not a jj repository
- User explicitly requests discussion or experimentation without snapshotting
- Working on untracked files outside
snapshot.auto-track patterns
Note: Unlike git, there is no staging area. All tracked files are always snapshotted.
Use .jjignore or .gitignore to prevent tracking unwanted files.
Non-interactive command execution
CRITICAL for AI agents and automation: Many jj commands launch interactive editors by default, causing execution to hang. ALL commands must be made non-interactive through explicit flags.
Commands requiring explicit flags for non-interactive use
| Command | Interactive behavior | Non-interactive pattern | Notes |
|---|
jj describe | Opens editor for description | jj describe -m "message" | Required |
jj describe -r <c> | Opens editor for description | jj describe -r <c> -m "message" | Required |
jj split <paths> | Opens editor twice: once for the extracted commit, once for the remainder at @ | Pre-stage @'s description first: jj describe -m "<remainder>" then jj split <paths> -m "<extracted>" | Two commit boundaries — a single -m covers only the extracted commit; the remainder still opens an editor without pre-staging. See "Common gotchas". |
jj split (no paths) | Opens diff editor (TUI) | Cannot be non-interactive | Avoid in automation |
jj squash -r <c> | Usually safe, may prompt | jj squash -r <c> | Generally OK without -m |
jj squash --into <dest> | Opens description merge editor when both source and destination have non-empty descriptions | jj squash --into <dest> -u -- <paths> (keeps dest description) or jj squash --into <dest> -m "msg" -- <paths> (sets description) | -u preserves existing description; -m replaces it. -u is preferred when routing into commits that already have descriptions. |
jj new | No editor (safe) | jj new or jj new -m "msg" | Safe as-is |
Mandatory command verification protocol
Before executing ANY jj command in automated context:
- If uncertain about interactivity: Run
jj [subcommand] --help FIRST
- Check help output for:
-m, --message <MESSAGE> flag (indicates editor can be launched)
- Keywords: "editor", "interactive", "TUI", "prompt"
- Provide required parameters: Always via command-line flags, never rely on prompts
Common gotchas and solutions
jj split requires -m even with explicit paths:
jj split file.txt
jj split file.txt -m "refactor: extract file.txt changes"
jj split file1.txt file2.txt
jj split file1.txt file2.txt -m "feat: add new files"
Parameterless jj split cannot be made non-interactive:
jj split
jj split <specific-paths> -m "description"
The multi-boundary mental model:
Every commit boundary needs explicit message handling.
One -m flag covers exactly one boundary.
Most jj subcommands create a single boundary, but jj split is the notable exception: it creates two boundaries (the extracted commit and the remainder at @).
Pre-stage the remainder's description via jj describe -m "<remainder>" before jj split <paths> -m "<extracted>", otherwise the post-split editor invocation for @ still hangs the shell.
This rule generalizes: when a subcommand mutates more than one commit's description, every boundary needs its message provided in advance.
jj split file.txt -m "refactor: extract file.txt changes"
jj describe -m "wip: remaining changes"
jj split file.txt -m "refactor: extract file.txt changes"
jj describe without -m always opens editor:
jj describe
jj describe -m "feat: implement feature"
jj describe -r <commit>
jj describe -r <commit> -m "updated description"
Composite working copy maintenance:
When using the multi-parent development join (composite working copy) + wip pattern, the development join must have a description to prevent auto-abandonment.
Always work in the wip commit (@), never directly in the development join.
The shared empty [wip] at @ is the stable coordination point that makes N concurrent editors safe by construction: every editor — human or LLM agent — edits that same [wip], whose working tree reflects the integrated union of all chain contents, then routes each completed change downward into the owning chain.
In this repo @/[wip] is additionally tracked by the pushed wip deploy bookmark, and machines rebuild from it, so @ must never drift off [wip].
Operations that drift @ away from wip, and are therefore forbidden while a development join is present:
jj new <single-parent> and jj edit <other> move @ away from the join (record the join's change ID before such operations and restore with jj new <join-change-id> afterward).
jj describe @ ... consumes the empty wip into a content commit.
jj rebase -r @ --insert-before <target> / --insert-after <target> and jj rebase --revisions @ --insert-before/--insert-after <target> relocate the wip below or into the join interior.
Never describe @ into content and never positionally rebase @.
The one sanctioned jj rebase touching @ is the destination add/remove-chain form jj rebase -r @ -d 'all:(@- | new-bookmark)' (add a chain) or jj rebase -r @ -d 'all:(@- ~ removed-bookmark)' (remove a chain); the all: prefix forces a multi-parent merge so @ stays an empty direct child of the rebuilt join and is not drifted.
All content leaves @ by routing downward with @ left in place and empty, via these editor-safe verbs:
jj absorb (auto-distribute by blame; prefer the scoped jj absorb <path> form under concurrency) — safe from wip without --keep-emptied.
jj squash --from @ --into <chain-tip> --keep-emptied [-- <paths>] (amend-route; omit -m to preserve the tip description — the empty wip carries no description, so the description-merge editor never fires).
jj squash --from @ --insert-after <chain-tip> -m "feat(scope): description" --keep-emptied [-- <paths>] (append-route; then advance the bookmark to the Created new commit <id> line jj prints, via jj bookmark move <chain> --to <id>).
jj squash --from @ --insert-before <target> -m "fix(scope): description" --keep-emptied -- <paths> (splice-below-join from live @; this is the correct mechanic for routing a base-bound fix into the splice region — it never moves @).
jj split keeping the wip remainder (do not pre-stage by describing @ in a join — that consumes the wip; pass explicit paths and -m per the split guidance above).
The splice-below-join must use the --keep-emptied squash form, never jj describe @ followed by jj rebase --revisions @ --insert-before <target>: that two-step opens a transient window with no [wip] on the join, which is catastrophic under concurrency, and in this repo drags the pushed wip deploy bookmark below the join.
In every splice/relocation recipe the relocated <commit>/<X>/<range> is a separate, already-sealed non-wip commit, never @/[wip] itself.
For the canonical invariant statement (iii-b), command templates, and rationale, see ~/.claude/skills/jj-version-control/SKILL.md §"Development join".
Git parity and the jj new requirement
Critical understanding: jj working copy @ exists only in .jj/ metadata until explicitly frozen.
The problem:
- Described commits in jj
@ are NOT automatically exported to git
- From git's perspective,
@ appears as uncommitted working directory changes
- This breaks git-based tooling, CI/CD, and collaboration workflows
The solution - atomic commit pattern with immediate git export:
jj describe -m "feat: implement feature X"
jj new
Without jj new:
jj describe -m "feat: done"
jj git push --bookmark main
With jj new:
jj describe -m "feat: done"
jj new
jj git push --bookmark main
When to use jj new:
- After every
jj describe -m "message" for atomic commits
- When preparing commits for git operations (push, PR creation, etc.)
- When you want to checkpoint work and start the next logical change
When jj new is optional:
- During rapid experimentation where git visibility doesn't matter
- When using jj-only features (workspaces, operation log recovery)
- Will eventually need it before any git interaction
Escape hatches for interactive operations
If interactive command unavoidable:
- Warn user that manual interaction required
- Provide exact command for user to run
- Document why automation cannot handle it
- Never execute commands with
-i or --interactive flags in automation
JJ_EDITOR=true belt-and-suspenders pattern:
For untrusted call sites or extra paranoia at any commit boundary, prefix the jj invocation with JJ_EDITOR=true.
true is a no-op shell builtin that exits 0 immediately, so any unexpected editor invocation completes without hanging.
The trade-off is that if jj genuinely needs a description and no -m is provided, the result is an empty description rather than a hang — which is recoverable, unlike a stuck shell in a subagent context.
JJ_EDITOR=true jj squash --from <fixup> --into <target> --use-destination-message
JJ_EDITOR=true jj split file.txt -m "refactor: extract"
Verify post-op state with jj log and the relevant commit's description.
If an empty description landed where one was needed, recover with jj op restore <pre-op-id> (identify via jj op log --limit 8) and retry with the proper -m flag(s).
In-flight hang recovery (SIGTERM + jj op restore):
When an editor is already hanging in a non-interactive shell, the recovery procedure is:
- From another shell, identify the editor process:
ps -ef | grep -E "(nvim|vim|nano|hx|emacs)" | grep -v grep. Look for the process whose argv references a .jjdescription temp file path — that's the one jj is waiting on.
- SIGTERM the editor PID:
kill <pid>. Avoid kill -9 (SIGKILL); the editor's atexit handler may need to clean up its swap file, and SIGKILL skips that.
- The stuck bash command returns non-zero; jj typically aborts the operation but may leave a malformed commit if it had begun mutating state.
- Inspect operation state:
jj op log --no-graph -T 'separate(" ", id.short(), description.first_line()) ++ "\n"' --limit 8.
- If state is bad, restore:
jj op restore <pre-bad-op-id>.
Foundation: Atomic commit workflow
Working copy commit behavior
The working copy is always the @ commit:
- All file changes automatically amend
@ without explicit commands
@ is rewritten in place as you work (new commit ID, same change ID)
- Use
jj describe -m "message" to add description when changes represent cohesive unit
- Required:
-m flag for non-interactive execution (never omit it)
- Use
jj new to freeze @ and create new empty @ on top
- Git export: Frozen commits become visible in git;
@ remains jj-only until frozen
- Atomic workflow:
jj describe -m "msg" → jj new → commit exported to git, ready for next change
- Use
jj commit to move @ changes into its parent (alternative to jj new)
Critical for git parity:
Without jj new, your described working copy commit exists only in jj's .jj/ directory.
From git's perspective, these changes remain uncommitted in the working directory.
Execute jj new after each jj describe -m "msg" to maintain git/jj synchronization.
Organizing changes:
- Let related changes accumulate in
@
- Use
jj split <paths> -m "message" when changes diverge into separate concerns
- Required:
-m flag even when providing paths (common mistake to omit)
- Use
jj squash to move changes between commits
- Use
jj absorb to automatically distribute fixes to appropriate ancestors
File state awareness:
- Run
jj status to see what's in current @
- Run
jj diff to review changes in @
- No staging area to check - working copy state is commit state
Bookmark management
Bookmarks are named pointers that don't move automatically with new commits.
Core behavior:
- Bookmarks stay on their target when you create new commits (unlike git branches)
- Bookmarks only move when commits are rewritten (rebase, squash, abandon)
- Update bookmarks explicitly:
jj bookmark set <name> -r <commit>
- Create bookmarks for important points:
jj bookmark create <name> -r <commit>
- Always work in "detached HEAD" state - this is normal in jj
Naming conventions:
- Main bookmarks:
main, beta, staging
- Feature work:
issue-N-descriptor (e.g., issue-42-add-auth)
- Experiments:
exp-N-description (e.g., exp-1-refactor-parser)
- Archives:
archive/old-bookmark-name
Integration with issue tracking:
- When work diverges from current bookmark's purpose, create new bookmark
- Example: working near
issue-42-auth but fixing unrelated bug → jj bookmark create issue-58-logging
Default bias: bookmarks are cheap, use them liberally to mark important commits.
Operation log and recovery
Every jj operation is atomic and recorded in the operation log.
Core commands:
jj undo - undo last operation (any operation, not just commits)
jj op log - view complete operation history
jj op restore <id> - restore repo to exact prior state
jj op show <id> - see what an operation changed
Recovery patterns:
- Mistake in last operation:
jj undo
- Mistake several operations ago:
jj op log, then jj op restore <id>
- Want to undo operation N but keep N+1:
jj op restore to N-1, manually redo N+1
- Concurrent operations created divergence: inspect with
jj log, resolve with jj bookmark set
Key insight: Operation log is your safety net, not backup branches.
Delete bookmarks freely - commits remain in operation log.
Conflict management
Conflicts are first-class citizens, committed and resolved when convenient.
Conflict workflow:
- Operations never fail due to conflicts - conflicts are committed with marker
- Continue working on other commits while conflicts exist
- View conflicted commits:
jj log -r 'conflict()'
- Resolve when ready:
jj new <conflicted-commit>, fix files, jj squash resolution back
- Or resolve in place:
jj edit <conflicted-commit>, fix files (automatically amends)
Conflict tools:
jj resolve - launch merge tool for each conflict
jj resolve --list - see all conflicts in current commit
- Edit conflict markers directly in files or use merge tools
Never blocked by conflicts - they're just another commit state.
Parallel experimentation with bookmarks
Start with bookmarks in single workspace. Graduate to separate workspaces only when needed.
Starting experiments from main
jj bookmark create exp-1-nix-flakes -r main
jj bookmark create exp-2-home-manager -r main
jj bookmark create exp-3-unified-config -r main
jj new exp-1-nix-flakes
jj describe -m "[exp-1] feat(nix): migrate to flakes - part 1"
jj new
jj describe -m "[exp-1] feat(nix): migrate to flakes - part 2"
jj new
jj new exp-2-home-manager
jj describe -m "[exp-2] feat(home): initial home-manager setup"
jj new
Viewing and comparing experiments
Query experiments using revsets:
jj log -r 'main.. & (exp-1-nix-flakes:: | exp-2-home-manager:: | exp-3-unified-config::)'
jj log -r 'main..exp-1-nix-flakes'
jj diff -r 'main..exp-1-nix-flakes'
jj log -r '(main..exp-1-nix-flakes) ~ (main..exp-2-home-manager)'
jj log -r '(main..exp-1-nix-flakes) & (main..exp-2-home-manager)'
jj log -r 'main..exp-1-nix-flakes' --no-graph --template 'commit_id ++ "\n"' | wc -l
Checkpoint and push experiments
jj bookmark set exp-1-nix-flakes -r @-
jj git push --bookmark exp-1-nix-flakes
Advantages of bookmark-only experiments
- Minimal overhead (no separate directories)
- Fast switching between experiments
- Operation log tracks everything
- No stale workspace issues
- Lower disk space usage
Disadvantages:
- Working tree changes when switching (like git checkout)
- Cannot run tests in parallel
- Cannot compare files side-by-side easily
Workspace creation (explicit user request only)
Workspace creation is reserved for cases where the user explicitly requests workspace isolation in-session.
Parallel related work in jj mode uses the diamond workflow's development join, not workspace creation.
Cross-references:
~/.claude/skills/jj-version-control/tiered-ceremony.md — policy authority for when ceremony escalates from anonymous chain to bookmarked chain to workspace.
~/.claude/skills/jj-version-control/diamond-workflow.md — canonical parallel-work alternative using multi-parent @.
~/.claude/skills/jj-version-control/SKILL.md "Development join" — the multi-parent working-copy entity that replaces workspaces for parallel work.
Trigger condition
The agent creates a workspace only when the user explicitly requests it in-session.
Explicit request means an utterance naming worktree, workspace, isolate, or separate working copy, or a path form such as .worktrees/X or ../myproject-exp-1.
In the absence of such an utterance, parallel related work uses the development join described in the diamond workflow, and serial work uses anonymous chains or bookmarks on a single working copy.
Examples of why a human might request workspace isolation (long-running builds undisturbed by editing, side-by-side file comparison in editors, divergent sparse-checkout patterns) are informative context for the human's decision, not classification criteria the agent evaluates.
For example, a user may request a workspace to keep a long-running build process undisturbed by ongoing edits, but the trigger remains the explicit request, not the inferred build duration.
Creating a workspace when requested
jj workspace add ../myproject-exp-1 -r exp-1-nix-flakes
cd ../myproject-exp-1
jj describe -m "[exp-1] feat(nix): add flake inputs"
jj new
nix build .# &
Workspace-specific concepts
Working-copy commits:
- Each workspace has unique working-copy commit tracked in repository view
@ refers to current workspace's working-copy commit
exp1@ refers to workspace named "exp1" working-copy commit
jj log -r 'working_copies()' shows all workspace working-copy commits
Stale workspaces:
- Workspace becomes stale when its working-copy commit is rewritten from another workspace
- Example: rebase
exp1@ from primary workspace → exp1 workspace becomes stale
- Warning appears when running commands in stale workspace
- Fix with
jj workspace update-stale to update files to new commit
Cross-workspace operations:
- Can work near same bookmark from multiple workspaces (no exclusive ownership)
- Can read/query any commit from any workspace
- Rewriting another workspace's working-copy commit makes it stale
- Safe:
jj new exp1@ (create new commit on top)
- Risky:
jj edit exp1@ (edit directly, will cause staleness if files change)
Workspace lifecycle
jj workspace list
jj workspace add <path> -r <starting-commit>
jj workspace forget <workspace-name>
rm -rf <workspace-path>
myproject/
myproject-exp-1/
myproject-exp-2/
Experiment lifecycle management
Scale to arbitrary number of experiments with minimal complexity.
Naming conventions
Bookmarks:
exp-{number}-{short-description}
- Examples:
exp-1-refactor-parser, exp-2-add-async, exp-3-optimize-cache
Workspaces (when needed):
{repo}-exp-{number}
- Examples:
myproject-exp-1, myproject-exp-2
Commit descriptions:
- Active:
[exp-N] type: description
- Review:
[exp-N:review] type: description
- Archived:
[exp-N:archived] type: description
- Winner:
[exp-N:winner] type: description
Revset aliases for scaling
Add to ~/.jjconfig.toml:
[revset-aliases]
'exps()' = 'main.. & ~main'
'exp(x)' = 'main..x'
'exp_diff(x, y)' = '(main..x) ~ (main..y)'
'exp_shared(x, y)' = '(main..x) & (main..y)'
'wcs()' = 'working_copies()'
'exp_wcs()' = 'working_copies() & ~default@'
Usage:
jj log -r 'exps()'
jj log -r 'exp(exp1@)'
jj log -r 'exp_diff(exp1@, exp2@)'
jj log -r 'exp_shared(exp1@, exp2@)'
Experiment registry
Maintain docs/experiments.md in repository:
# Experiments Registry
## Active
### exp-1-refactor-parser
- Status: Active
- Bookmark: `exp-1-refactor-parser`
- Workspace: `myproject-exp-1`
- Goal: Rewrite parser for 10x performance
- Created: 2025-10-15
- PR: #123
### exp-2-add-async-support
- Status: Active
- Bookmark: `exp-2-add-async-support`
- Workspace: None (bookmark only)
- Goal: Add async/await throughout
- Created: 2025-10-14
- PR: None yet
## Review
### exp-3-optimize-cache
- Status: Ready for review
- Bookmark: `exp-3-optimize-cache`
- Workspace: `myproject-exp-3`
- Goal: LRU cache for 30% speedup
- Created: 2025-10-10
- PR: #120
## Archived
### exp-4-graphql-api
- Status: Archived (too complex)
- Bookmark: `archive/exp-4-graphql-api`
- Workspace: Removed
- Goal: GraphQL API layer
- Created: 2025-10-01
- Archived: 2025-10-12
- Reason: Scope too large, splitting into smaller experiments
## Integrated
### exp-5-docker-support
- Status: Merged to main
- Bookmark: Deleted
- Goal: Add Docker deployment
- Created: 2025-09-20
- Merged: 2025-10-01
- PR: #105
State transitions and lifecycle
jj describe -r 'description(glob:"*[exp-N]*")' -m "[exp-N:review] ..."
jj git push --bookmark exp-N
jj rebase -s 'main..exp-N' -d main
jj bookmark set main -r exp-N
jj git push --bookmark main
jj bookmark rename exp-N archive/exp-N
jj bookmark delete archive/exp-N
jj log -r 'description(glob:"*[exp-*:review]*")'
jj log -r 'description(glob:"*[exp-*:archived]*")'
Periodic cleanup
jj bookmark list | grep '^exp-'
jj log -r 'bookmarks() & description(glob:"exp-*")' \
--template 'bookmarks ++ " " ++ committer.timestamp() ++ "\n"'
jj bookmark rename exp-old archive/exp-old
jj abandon 'empty() & exps()'
jj op log --limit 50
jj op restore <op-id>
Experiment dependencies
Stack experiments when one depends on another:
jj bookmark create exp-1 -r @
jj new exp-1
jj describe -m "[exp-2] feat: builds on exp-1"
jj bookmark create exp-2 -r @
jj rebase -r exp-1 -d main
Combine experiments in a development join when needing multiple:
jj new exp-1 exp-2
jj describe -m "[exp-3] feat: combines exp-1 and exp-2"
jj bookmark create exp-3 -r @
Cherry-pick specific commits:
jj new main
jj squash --from <specific-commit> --into @
jj describe -m "[exp-4] feat: uses technique from exp-1"
Multi-parent working copy
A multi-parent @ merges multiple bookmarks into a single working tree, providing the same capability as GitButler's applied-branches workspace.
All parent bookmarks are visible and editable simultaneously without worktrees or filesystem separation.
The resulting @ is a development join.
When a parent bookmark advances (via squash --into, absorb, commits from another workspace, collaborator push, or jj git fetch), jj automatically rebases @ onto the updated parents.
The development join's working tree stays current without manual rebase steps.
For the operational workflow, conflict semantics, edit-route cycle, route-and-extend pattern, composite-maintenance invariant, and beads-integration recipes, see ~/.claude/skills/jj-version-control/SKILL.md §"Development join".
For the cross-tool terminology mapping ("GitButler equivalence mapping"), see ~/.claude/skills/preferences-git-version-control/03-jj-mode.md.
For background on multi-parent jj new and the design rationale, see Chris Krycho's "jj init" essay, particularly the section on creating three-parent merges.
History refinement
Transform experimental development history into clean, reviewable commit sequence.
Principles:
- Operations execute immediately and atomically
- No special modes or interactive editors
- Operation log is safety net (not backup branches)
- Descendants auto-rebase when ancestors change
- Conflicts are committed, not blocking
Incremental cleanup workflow
jj log -r 'main..@'
jj squash -r 'description(glob:"fixup*") & main..@'
jj squash -r 'description(glob:"oops*") & main..@'
jj abandon 'empty() & main..@'
jj squash -r 'description(glob:"WIP:*") & main..@'
jj log -r 'main..@'
jj rebase -r <commit> -d <new-parent>
jj rebase -r <commit> -A <after>
jj rebase -r <commit> -B <before>
jj describe -r <commit> -m "proper conventional commit message"
jj log -r 'main..@'
jj diff -r 'main..@'
Each step executes immediately. Use jj undo to back out of any step.
Core operations
These rebase-by-revision recipes assume <commit> is a stacked, non-wip commit in a single chain.
When a multi-parent development join is present (see "Composite working copy maintenance" above and §"Diamond workflow"), never pass @ (the empty [wip]) as the rebased revision: relocating @ below the join destroys the shared editing surface concurrent actors write to and drags the pushed wip deploy bookmark.
To splice a fix below the join from the working copy, use jj squash --from @ --insert-before <target> -m "msg" --keep-emptied -- <paths>, which leaves @ in place and empty; see the splice-below-join recipe in ~/.claude/skills/jj-version-control/SKILL.md §"Development join".
Reorder commits:
jj rebase -r <commit> -d <parent>
jj rebase -r <commit> -A <after-commit>
jj rebase -r <commit> -B <before-commit>
jj rebase -s <commit> -d <new-base>
Note on -A and -B: These flags automatically rebase all descendants of the moved commit onto it in a single atomic operation. No second rebase command needed to reconnect the chain.
Using change ID prefixes: For faster typing, use unambiguous prefixes of change IDs:
jj rebase -r tknpxpos -A ynrpuxsz
jj rebase -r tk -A y
Moving ranges vs single commits: Choose between -r and -s based on what you're moving:
jj rebase -r <commit> -A <after>
jj rebase -s <first-commit> -d <destination>
Use -r with -A/-B when moving one commit within a chain. Use -s when you want to move multiple commits together as a unit - the specified commit plus everything built on top of it will move to the new destination. This is the primary way to "slide" a range of commits through history.
Squash commits:
jj squash -r <commit>
jj squash -r <commit> -m "combined message"
jj squash --from <commit> --into <ancestor>
jj squash -i -r <commit>
jj squash -r 'description(glob:"WIP:*") & main..@'
Drop commits:
jj abandon <commit>
jj abandon 'description(glob:"tmp:*") & main..@'
jj abandon 'empty() & main..@'
jj abandon <start>::<end>
Reword descriptions:
jj describe -r <commit> -m "new description"
jj describe -r <commit>
jj describe -r 'description(glob:"WIP*") & main..@' -m "proper: description"
jj describe -r <commit> -m ""
Split commits:
jj split <paths> -m "description for selected changes"
jj split src/feature.rs -m "feat: implement feature X"
jj split file1.txt file2.txt -m "docs: add documentation"
jj split -r <commit> <paths> -m "refactor: extract changes"
jj split -r <commit>
jj split <paths>
jj split -i -r <commit>
jj split <paths> -m "description"
CRITICAL: jj split requires -m "message" flag when providing paths to avoid launching
editor for description. Without -m, the command succeeds in selecting files but then
blocks waiting for interactive description input. This is the most common mistake.
After splitting, remember to freeze commits for git export:
jj split file.txt -m "refactor: extract changes"
jj describe -m "refactor: remaining changes"
jj new
Edit commit content:
jj edit <commit>
jj new @-
jj diffedit -r <commit>
jj edit <commit>
jj commit <files>
jj new @-
Auto-distribute changes with absorb
Automatically move fixes to commits that last touched those lines:
jj absorb
Most powerful for fixing issues found during review.
Verification workflow
After cleanup:
jj log -r 'main..@'
jj diff -r 'main..@'
for commit in $(jj log -r 'main..@' --no-graph --template 'commit_id ++ "\n"'); do
jj new $commit --no-edit
nix build .# || echo "Build failed: $commit"
done
jj new @-
jj log -r 'conflict() & main..@'
jj op log --limit 20
Complete cleanup example
Starting state:
jj log -r 'main..@'
Goal: 2 clean commits (one per feature)
jj squash --from zzz999 --into abc123
jj abandon yyy888
jj squash -r xxx777
jj squash -r def456
jj squash -r jkl012
jj squash -r mno345
jj describe -r abc123 -m "feat: implement feature X with tests"
jj describe -r ghi789 -m "feat: implement feature Y with error handling"
jj log -r 'main..@'
jj new abc123 && nix build .# && jj new @-
jj new ghi789 && nix build .# && jj new @-
jj bookmark set feature-xy -r @
If any step fails, jj undo backs out immediately.
Advanced patterns
Diamond workflow (epic-scoped)
The diamond workflow connects beads epic issue graphs to jj chain topology through four phases: diverge, develop, converge, serialize.
Tactical commands at use-sites include jj new chain-a chain-b ... for the development join, jj describe -m "join N=<cardinality>: <alphabetical bookmarks, comma-separated>" then jj new for the join + wip structure, and jj squash --from <src> --into <chain> for routing changes.
For the canonical operational recipe, theoretical foundations, and beads-to-jj mapping, see ~/.claude/skills/jj-version-control/diamond-workflow.md.
Integration strategies
Rebase winner onto main (preserves commit history):
jj rebase -s 'main..exp-1-winner' -d main
jj bookmark set main -r exp-1-winner
jj git push --bookmark main
Squash winner into main (clean single commit):
jj new main
jj squash --from 'main..exp-1-winner' --into @
jj describe -m "feat: feature from experiment 1
Squashed from exp-1-winner.
Includes: ..."
jj bookmark set main -r @
jj git push --bookmark main
Integrate multiple experiments (when both valuable):
For dissolving a development join and sequential rebase linearization of N chains onto main, see ~/.claude/skills/jj-version-control/SKILL.md §"Integration strategies at completion" (canonical entity reference) and ~/.claude/skills/jj-version-control/diamond-workflow.md §"Phase 4: serialize (integrate)" (full recipe including N+1 stacked-base PR submission).
Sub-experiments within experiments
jj bookmark create exp-1-parser -r main
jj new exp-1-parser
jj describe -m "[exp-1a] feat: regex parser"
jj bookmark create exp-1a-regex -r @
jj new exp-1-parser
jj describe -m "[exp-1b] feat: PEG parser"
jj bookmark create exp-1b-peg -r @
jj diff -r 'exp-1-parser..exp-1a-regex'
jj diff -r 'exp-1-parser..exp-1b-peg'
jj rebase -s exp-1b-peg -d main
jj bookmark set exp-1-parser -r exp-1b-peg
jj bookmark delete exp-1a-regex
Experimental feature flags
Merge experiment to main early behind feature flag:
jj new main
jj describe -m "[exp-4] feat: add experimental cache
Behind EXPERIMENTAL_CACHE feature flag."
jj bookmark set main -r @
jj git push --bookmark main
jj workspace add ../myproject-exp-4-cache -r @
Session workflow pattern
Effective jj session:
jj git fetch
jj log
jj new <base>
jj describe -m "message"
jj split
jj squash
jj undo
jj log -r 'main..@'
jj describe -r <commits>
jj bookmark set <name> -r @
jj git push --bookmark <name>
jj op log
jj op restore <id>
Git colocated mode
Jujutsu operates in colocated mode with existing git repositories, allowing seamless interoperation.
Initializing colocated repository
cd /path/to/git/repo
jj git init --colocate
ls -la
jj log
Alternative initialization:
jj git clone <url> <directory>
jj git init --git-repo=.
Colocated workflow
In colocated mode:
- All jj bookmark operations automatically sync to git branches
- Git commands work on same repository - changes appear in
jj log
jj git import imports git changes (automatic in colocated mode)
jj git export exports to git refs (automatic in colocated mode)
- Git-specific features (GitHub PRs, etc.) work with jj-managed bookmarks
Workflow:
- Prefer jj commands for all operations
- Git commands work but are less powerful
- After git operations, check
jj log to see imported changes
- Operation log tracks git operations as "import git refs"
Note on detached HEAD:
jj operates in "detached HEAD" mode (git terminology). This is normal and does not affect jj operations - jj git push --bookmark <name> works correctly without an attached HEAD. Only attach HEAD if switching to git-native workflows:
git checkout main
git switch main
jj operations may detach HEAD again. This is expected behavior.
Remote synchronization
jj git fetch --all-remotes
jj bookmark track <name>@<remote>
jj git push --bookmark <name>
jj git push --all
jj git push --tracked
Log visibility and remote bookmarks
After pushing commits to a remote, jj log hides them by default to focus on work-in-progress.
Symbols indicating commit state:
○ - Mutable commit (local, not pushed)
◆ - Immutable commit (exists on remote)
~ - History truncated (more commits exist but are hidden)
jj git push --bookmark main
jj log
jj log -r 'all()'
jj log -r '::@'
jj log -r 'main@origin::'
Customizing log visibility:
To always show full history, configure default revset in ~/.jjconfig.toml:
[revsets]
log = "@ | ancestors(bookmarks() | tags() | remote_bookmarks(), 2)"
Or create an alias for full history:
[aliases]
la = ["log", "-r", "all()"]
This default behavior keeps your log focused on uncommitted work while clearly marking the boundary between local changes and pushed commits.
Reverting to git-only operations
Since colocated mode maintains both .git/ and .jj/, you can revert to git-only operations at any time:
git status
git log
git checkout <branch>
rm -rf .jj/
Key insight: Colocated mode provides a safe, reversible migration path.
Your git repository is never at risk - jj adds capabilities without replacing git.
Reference
Essential revset patterns
Query language for selecting commits:
@
<workspace>@
<bookmark>
<bookmark>@<remote>
root()
A..B
A::B
A | B
A & B
~A
A ~ B
mine()
author("pattern")
description(glob:"pattern")
file("path")
~/path
empty()
conflict()
merge()
bookmarks()
working_copies()
main..@
mine() & ~bookmarks()
description(glob:"WIP*") & main..@
conflict() & main..@
Common command patterns
jj new <base>
jj new <parent1> <parent2>
jj describe -m "message"
jj commit
jj split
jj edit <commit>
jj squash
jj squash -r <commit>
jj squash --from <src> --into <dst>
jj squash -i
jj absorb
jj rebase -r <commit> -d <dest>
jj rebase -s <commit> -d <dest>
jj rebase -r <commit> -A <after>
jj rebase -r <commit> -B <before>
jj abandon <commit>
jj bookmark create <name>
jj bookmark create <name> -r <commit>
jj bookmark set <name> -r <commit>
jj bookmark rename <old> <new>
jj bookmark delete <name>
jj bookmark list
jj bookmark track <name>@<remote>
jj workspace add <path> -r <commit>
jj workspace list
jj workspace forget <name>
jj workspace update-stale
jj log
jj log -r <revset>
jj diff -r <commit>
jj show <commit>
jj evolog -r <commit>
jj op log
jj op show <id>
jj undo
jj op restore <id>
jj git init --colocate
jj git clone <url>
jj git fetch
jj git push --bookmark <name>
jj git push --all
Session operation summary
After working session, provide summary:
jj op log --limit 20
jj log -r 'bookmarks()..@'
jj op log --limit N
jj log -r 'main..@'
Use explicit operation IDs from session start for precise ranges.
Key principles
- Non-interactive execution required: Always use
-m flag with commands that can prompt (jj describe, jj describe -r, jj split <paths>); verify unfamiliar commands with jj [subcommand] --help before execution
- Git parity via jj new: Working copy
@ is not exported to git until frozen; execute jj new after jj describe -m "msg" to make commits visible in git and maintain synchronization
- Command verification protocol: When uncertain if a command is non-interactive, run
--help first and check for -m, --message flag or interactive keywords
- Operation log is history (commits are snapshots, operations are timeline)
- Bookmarks don't move automatically (only on commit rewrites)
- Working copy commit
@ is ephemeral in solo work (constantly rewritten); in a multi-parent development join it is held STABLE as the empty [wip] coordination point — do not treat it as freely rewritable (see "Composite working copy maintenance")
@ is the stable [wip] in a development join: when a multi-parent join is active, @ is the empty [wip] shared by all concurrent editors and tracked by the pushed wip deploy bookmark (machines rebuild from it); route changes DOWN with jj absorb or jj squash --from @ --insert-before/--insert-after <target> -m "msg" --keep-emptied [-- <paths>], and never jj describe @ into content nor jj rebase -r @ / jj rebase --revisions @ — either drifts @ off the join, destroys the shared editing surface, and drags the deploy bookmark (see ~/.claude/skills/jj-version-control/SKILL.md §"Development join")
- Change IDs provide stability (commit IDs change, change IDs don't)
- No staging area (working copy state is commit state)
- Conflicts are first-class (committed, resolved when convenient)
- No special modes (all operations execute immediately)
- Start with bookmarks (graduate to workspaces when needed)
- Describe atomically (one logical change per description)
- Trust operation log (delete bookmarks freely)
- Use revsets liberally (query, don't manually track)
- Clean up incrementally (jj undo backs out any step)