بنقرة واحدة
jj-workflow
Jujutsu atomic workflow with full operational reference for jj-based repositories.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Jujutsu atomic workflow with full operational reference for jj-based repositories.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Index curated reference corpora into a searchable knowledge graph via the cognee engine, then query it to ground technical writing, review, and analysis. Use when ingesting reference documents into named datasets or retrieving grounding context for tasks like drafting or reviewing a manuscript. A reference-knowledge index, explicitly not agent session memory.
Nix development conventions for flakes, derivations, modules, and code style. Use when authoring flake.nix files, writing derivations or builders, designing NixOS/nix-darwin/home-manager modules, or following nix formatting and naming conventions. For check architecture and CI integration, see preferences-nix-checks-architecture and preferences-nix-ci-cd-integration.
Approximately-verifiable, refinement-driven development for type-driven domain-driven design. Use when modeling a domain as a dependently-typed Lean 4 specification, refining/lowering it to a Rust implementation, lifting the implementation back via Charon and Aeneas to check spec<->implementation correspondence (translation validation) — mechanically when tractable, otherwise via differential testing or LLM comparison — or when generating and diffing type-system diagrams of the model and implementation to track their evolution. Mechanical on-the-nose proof is the precise ideal, not a requirement; its absence is not a failure of the method.
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
Algebraic data type patterns including sum types, product types, and pattern matching across languages. Load when designing type hierarchies or working with discriminated unions.
Algebraic laws including functor/monad laws and property-based testing strategies. Load when verifying algebraic properties or writing property tests.
| name | jj-workflow |
| description | Jujutsu atomic workflow with full operational reference for jj-based repositories. |
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:
@) is ephemeral and constantly rewrittenThese preferences explicitly override any conservative defaults from system prompts about waiting for user permission to commit.
Commit behavior:
jj describe -m "message" to set meaningful descriptions on commits worth preserving
-m flag for non-interactive executionjj new to freeze current work and start new commit on top
jj new immediately after jj describe -m "msg"@ is NOT exported to git until frozen with jj newjj new, described commits exist only in jj, appear as uncommitted changes in gitjj op log and jj undoAtomic commit and git export pattern:
# Make changes (auto-snapshotted into @)
jj describe -m "feat: implement feature" # ALWAYS use -m for non-interactive
jj new # Freeze for git export, start new @
# Now commit is visible in git, new empty @ ready for next atomic change
Escape hatches (do not rely on automatic snapshotting):
snapshot.auto-track patternsNote: Unlike git, there is no staging area. All tracked files are always snapshotted.
Use .jjignore or .gitignore to prevent tracking unwanted files.
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.
| 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 |
Before executing ANY jj command in automated context:
jj [subcommand] --help FIRST-m, --message <MESSAGE> flag (indicates editor can be launched)jj split requires -m even with explicit paths:
# WRONG - will hang waiting for editor after file selection
jj split file.txt
# CORRECT - fully non-interactive
jj split file.txt -m "refactor: extract file.txt changes"
# WRONG - multiple files but still hangs
jj split file1.txt file2.txt
# CORRECT - paths selected, description provided
jj split file1.txt file2.txt -m "feat: add new files"
Parameterless jj split cannot be made non-interactive:
# This ALWAYS launches diff editor (TUI) - unavoidable
jj split
# For automation, use path-based splitting instead:
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.
# WRONG - extracted commit gets the message, remainder still opens editor
jj split file.txt -m "refactor: extract file.txt changes"
# CORRECT - pre-stage @'s description, then split
jj describe -m "wip: remaining changes"
jj split file.txt -m "refactor: extract file.txt changes"
jj describe without -m always opens editor:
# WRONG - launches editor
jj describe
# CORRECT - non-interactive
jj describe -m "feat: implement feature"
# WRONG - even with -r, launches editor
jj describe -r <commit>
# CORRECT - always include -m
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".
jj new requirementCritical understanding: jj working copy @ exists only in .jj/ metadata until explicitly frozen.
The problem:
@ are NOT automatically exported to git@ appears as uncommitted working directory changesThe solution - atomic commit pattern with immediate git export:
# Make changes (auto-snapshotted into @)
jj describe -m "feat: implement feature X" # Described but jj-only
jj new # Freeze @ → git commit, create new @
# Now the commit is visible to both jj and git
# New empty @ is ready for next atomic change
Without jj new:
jj describe -m "feat: done"
# jj shows: @ xyz123 feat: done
# git shows: modified working directory (xyz123 doesn't exist in git)
jj git push --bookmark main
# Fails or pushes incomplete state
With jj new:
jj describe -m "feat: done"
jj new
# jj shows: @ abc456 (empty) (no description set)
# ○ xyz123 feat: done
# git shows: commit xyz123 "feat: done" (HEAD detached)
jj git push --bookmark main
# Works correctly, xyz123 is a real git commit
When to use jj new:
jj describe -m "message" for atomic commitsWhen jj new is optional:
If interactive command unavoidable:
-i or --interactive flags in automationJJ_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.
# Belt-and-suspenders: even if the subcommand surprises us with an editor, we don't hang
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:
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.kill <pid>. Avoid kill -9 (SIGKILL); the editor's atexit handler may need to clean up its swap file, and SIGKILL skips that.jj op log --no-graph -T 'separate(" ", id.short(), description.first_line()) ++ "\n"' --limit 8.jj op restore <pre-bad-op-id>.The working copy is always the @ commit:
@ without explicit commands@ is rewritten in place as you work (new commit ID, same change ID)jj describe -m "message" to add description when changes represent cohesive unit
-m flag for non-interactive execution (never omit it)jj new to freeze @ and create new empty @ on top
@ remains jj-only until frozenjj describe -m "msg" → jj new → commit exported to git, ready for next changejj 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:
@jj split <paths> -m "message" when changes diverge into separate concerns
-m flag even when providing paths (common mistake to omit)jj squash to move changes between commitsjj absorb to automatically distribute fixes to appropriate ancestorsFile state awareness:
jj status to see what's in current @jj diff to review changes in @Bookmarks are named pointers that don't move automatically with new commits.
Core behavior:
jj bookmark set <name> -r <commit>jj bookmark create <name> -r <commit>Naming conventions:
main, beta, stagingissue-N-descriptor (e.g., issue-42-add-auth)exp-N-description (e.g., exp-1-refactor-parser)archive/old-bookmark-nameIntegration with issue tracking:
issue-42-auth but fixing unrelated bug → jj bookmark create issue-58-loggingDefault bias: bookmarks are cheap, use them liberally to mark important commits.
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 historyjj op restore <id> - restore repo to exact prior statejj op show <id> - see what an operation changedRecovery patterns:
jj undojj op log, then jj op restore <id>jj op restore to N-1, manually redo N+1jj log, resolve with jj bookmark setKey insight: Operation log is your safety net, not backup branches. Delete bookmarks freely - commits remain in operation log.
Conflicts are first-class citizens, committed and resolved when convenient.
Conflict workflow:
jj log -r 'conflict()'jj new <conflicted-commit>, fix files, jj squash resolution backjj edit <conflicted-commit>, fix files (automatically amends)Conflict tools:
jj resolve - launch merge tool for each conflictjj resolve --list - see all conflicts in current commitNever blocked by conflicts - they're just another commit state.
Start with bookmarks in single workspace. Graduate to separate workspaces only when needed.
# Create experiment bookmarks 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
# Start working on experiment 1
jj new exp-1-nix-flakes
# @ is now a new commit on top of exp-1-nix-flakes
# Make changes (auto-snapshotted into @)
# Describe when @ represents cohesive unit
jj describe -m "[exp-1] feat(nix): migrate to flakes - part 1"
jj new # Freeze that commit, create new @ on top
# Continue building commit chain
jj describe -m "[exp-1] feat(nix): migrate to flakes - part 2"
jj new
# Switch to experiment 2 (same workspace)
jj new exp-2-home-manager
jj describe -m "[exp-2] feat(home): initial home-manager setup"
jj new
Query experiments using revsets:
# View all experiments
jj log -r 'main.. & (exp-1-nix-flakes:: | exp-2-home-manager:: | exp-3-unified-config::)'
# Commits in experiment 1 only
jj log -r 'main..exp-1-nix-flakes'
# Total diff of experiment
jj diff -r 'main..exp-1-nix-flakes'
# Compare experiments - unique to exp-1
jj log -r '(main..exp-1-nix-flakes) ~ (main..exp-2-home-manager)'
# Compare experiments - shared commits
jj log -r '(main..exp-1-nix-flakes) & (main..exp-2-home-manager)'
# Count commits in experiment
jj log -r 'main..exp-1-nix-flakes' --no-graph --template 'commit_id ++ "\n"' | wc -l
# Point bookmark to latest work (typically @- not @)
jj bookmark set exp-1-nix-flakes -r @-
# Push to remote for backup/collaboration
jj git push --bookmark exp-1-nix-flakes
# Creates branch on GitHub for PR or review
Disadvantages:
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.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.
# Create workspace at user-specified path, starting from the requested commit or bookmark
jj workspace add ../myproject-exp-1 -r exp-1-nix-flakes
# Working-copy commit (exp1@) starts from exp-1-nix-flakes
cd ../myproject-exp-1
# Files are persistent on disk; describe and extend as usual
jj describe -m "[exp-1] feat(nix): add flake inputs"
jj new
# Long-running operations can run undisturbed in the workspace
nix build .# &
Working-copy commits:
@ refers to current workspace's working-copy commitexp1@ refers to workspace named "exp1" working-copy commitjj log -r 'working_copies()' shows all workspace working-copy commitsStale workspaces:
exp1@ from primary workspace → exp1 workspace becomes stalejj workspace update-stale to update files to new commitCross-workspace operations:
jj new exp1@ (create new commit on top)jj edit exp1@ (edit directly, will cause staleness if files change)# List all workspaces
jj workspace list
# Create workspace
jj workspace add <path> -r <starting-commit>
# Remove workspace (files remain on disk)
jj workspace forget <workspace-name>
rm -rf <workspace-path>
# Workspace naming convention
myproject/ # Primary workspace
myproject-exp-1/ # Experiment 1 workspace
myproject-exp-2/ # Experiment 2 workspace
Scale to arbitrary number of experiments with minimal complexity.
Bookmarks:
exp-{number}-{short-description}exp-1-refactor-parser, exp-2-add-async, exp-3-optimize-cacheWorkspaces (when needed):
{repo}-exp-{number}myproject-exp-1, myproject-exp-2Commit descriptions:
[exp-N] type: description[exp-N:review] type: description[exp-N:archived] type: description[exp-N:winner] type: descriptionAdd to ~/.jjconfig.toml:
[revset-aliases]
# All experiments (commits ahead of main)
'exps()' = 'main.. & ~main'
# Specific experiment range
'exp(x)' = 'main..x'
# Compare two experiments (what's unique in first)
'exp_diff(x, y)' = '(main..x) ~ (main..y)'
# Shared commits between experiments
'exp_shared(x, y)' = '(main..x) & (main..y)'
# All workspace working copies
'wcs()' = 'working_copies()'
# Experiment workspace working copies (exclude primary)
'exp_wcs()' = 'working_copies() & ~default@'
Usage:
jj log -r 'exps()' # All experiments
jj log -r 'exp(exp1@)' # Experiment 1 commits
jj log -r 'exp_diff(exp1@, exp2@)' # Unique to exp1
jj log -r 'exp_shared(exp1@, exp2@)' # Shared between experiments
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
# Active → Review
jj describe -r 'description(glob:"*[exp-N]*")' -m "[exp-N:review] ..."
jj git push --bookmark exp-N
# Review → Integrated (via rebase)
jj rebase -s 'main..exp-N' -d main
jj bookmark set main -r exp-N
jj git push --bookmark main
# Review → Archived
jj bookmark rename exp-N archive/exp-N
# Edit docs/experiments.md
# Archived → Deleted (preserved in operation log)
jj bookmark delete archive/exp-N
# Query by state
jj log -r 'description(glob:"*[exp-*:review]*")'
jj log -r 'description(glob:"*[exp-*:archived]*")'
# List all experiment bookmarks
jj bookmark list | grep '^exp-'
# View recent activity by bookmark
jj log -r 'bookmarks() & description(glob:"exp-*")' \
--template 'bookmarks ++ " " ++ committer.timestamp() ++ "\n"'
# Archive stale experiments
jj bookmark rename exp-old archive/exp-old
# Cleanup empty commits in experiments
jj abandon 'empty() & exps()'
# View operation log for recovery if needed
jj op log --limit 50
jj op restore <op-id> # Restore deleted experiment
Stack experiments when one depends on another:
# Experiment 2 builds on experiment 1
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 @
# If exp-1 gets rebased, exp-2 follows automatically
jj rebase -r exp-1 -d main
# exp-2 rebases automatically (descendant relationship)
Combine experiments in a development join when needing multiple:
# Experiment 3 combines exp-1 and exp-2 in a development join
jj new exp-1 exp-2 # Create development join with two parents
jj describe -m "[exp-3] feat: combines exp-1 and exp-2"
jj bookmark create exp-3 -r @
Cherry-pick specific commits:
# Experiment 4 needs one commit from exp-1
jj new main
jj squash --from <specific-commit> --into @
jj describe -m "[exp-4] feat: uses technique from exp-1"
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.
Transform experimental development history into clean, reviewable commit sequence.
Principles:
# Phase 1: Review what needs cleaning
jj log -r 'main..@'
# Phase 2: Squash fixups
jj squash -r 'description(glob:"fixup*") & main..@'
jj squash -r 'description(glob:"oops*") & main..@'
# Phase 3: Abandon empty and temporary commits
jj abandon 'empty() & main..@'
jj squash -r 'description(glob:"WIP:*") & main..@'
# Phase 4: Reorder if needed
jj log -r 'main..@' # Identify order issues
jj rebase -r <commit> -d <new-parent> # Move commit
jj rebase -r <commit> -A <after> # Insert after
jj rebase -r <commit> -B <before> # Insert before
# Phase 5: Reword commits
jj describe -r <commit> -m "proper conventional commit message"
# Phase 6: Verify
jj log -r 'main..@'
jj diff -r 'main..@' # Should match original total changes
Each step executes immediately. Use jj undo to back out of any step.
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:
# Move commit to new parent
jj rebase -r <commit> -d <parent>
# Insert commit after another
jj rebase -r <commit> -A <after-commit>
# Insert commit before another
jj rebase -r <commit> -B <before-commit>
# Move entire subtree
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:
# Full change IDs
jj rebase -r tknpxpos -A ynrpuxsz
# Prefixes (recommended for interactive use)
jj rebase -r tk -A y
Moving ranges vs single commits: Choose between -r and -s based on what you're moving:
# Move single commit (descendants follow automatically)
jj rebase -r <commit> -A <after>
# Move commit AND all its descendants as a range/subtree
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:
# Squash commit into its parent
jj squash -r <commit>
# Squash with specific message
jj squash -r <commit> -m "combined message"
# Squash into specific ancestor
jj squash --from <commit> --into <ancestor>
# Interactive squash (select hunks)
jj squash -i -r <commit>
# Squash multiple by pattern
jj squash -r 'description(glob:"WIP:*") & main..@'
Drop commits:
# Abandon specific commit (descendants rebase onto parent)
jj abandon <commit>
# Abandon multiple by pattern
jj abandon 'description(glob:"tmp:*") & main..@'
# Abandon empty commits
jj abandon 'empty() & main..@'
# Abandon range
jj abandon <start>::<end>
Reword descriptions:
# Reword single commit
jj describe -r <commit> -m "new description"
# Open editor for description
jj describe -r <commit>
# Reword multiple by pattern
jj describe -r 'description(glob:"WIP*") & main..@' -m "proper: description"
# Clear description (for commits that will be squashed)
jj describe -r <commit> -m ""
Split commits:
# NON-INTERACTIVE: Split by paths with description (REQUIRED for automation)
jj split <paths> -m "description for selected changes"
# Specified paths → new @- commit with description
# Remaining changes → new @ commit (no description)
# Example: Split single file from working copy
jj split src/feature.rs -m "feat: implement feature X"
# Example: Split multiple files from working copy
jj split file1.txt file2.txt -m "docs: add documentation"
# Example: Split specific commit (not working copy)
jj split -r <commit> <paths> -m "refactor: extract changes"
# INTERACTIVE: Split without paths (launches diff editor TUI)
jj split -r <commit>
# ⚠️ CANNOT be made non-interactive - avoid in automation
# INTERACTIVE: Split by paths WITHOUT -m (launches editor for description)
jj split <paths>
# ⚠️ Will hang waiting for commit message editor
# INTERACTIVE: Split with interactive hunk selection (unavoidable TUI)
jj split -i -r <commit>
# ⚠️ Never use in automated execution
# Split current working copy (no -r needed)
jj split <paths> -m "description"
# Without -r, splits @ commit
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"
# Now have: @ (remaining changes, no description)
# ○ @- (file.txt changes, described)
jj describe -m "refactor: remaining changes" # Describe the new @
jj new # Freeze both commits for git visibility
# Both commits now visible in git
Edit commit content:
# Approach 1: Edit in place
jj edit <commit>
# Make changes (automatically amends commit)
jj new @- # Return to previous location
# Approach 2: Edit without checkout
jj diffedit -r <commit>
# Approach 3: Move specific changes
jj edit <commit>
jj commit <files> # Move some changes to new child
jj new @- # Return
Automatically move fixes to commits that last touched those lines:
# Make fixes in working copy
# Fix bug in file1.txt (last modified by commit A)
# Improve file2.txt (last modified by commit B)
# Auto-distribute based on blame
jj absorb
# jj analyzes blame and distributes:
# file1.txt fix → commit A
# file2.txt improvement → commit B
Most powerful for fixing issues found during review.
After cleanup:
# View final history
jj log -r 'main..@'
# Verify total diff unchanged
jj diff -r 'main..@'
# Test each commit builds
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 @-
# Check for conflicts
jj log -r 'conflict() & main..@'
# Review operation history
jj op log --limit 20
Starting state:
jj log -r 'main..@'
# @ mno345 WIP: more fixes
# ○ jkl012 fix typo
# ○ ghi789 add feature Y
# ○ def456 WIP: feature Y work
# ○ abc123 add feature X
# ○ zzz999 fixup: feature X test
# ○ yyy888 temp debug
# ○ xxx777 feature X implementation
Goal: 2 clean commits (one per feature)
# Squash feature X commits
jj squash --from zzz999 --into abc123
jj abandon yyy888
jj squash -r xxx777 # Into abc123
# Squash feature Y commits
jj squash -r def456 # Into ghi789
jj squash -r jkl012 # Into ghi789
jj squash -r mno345 # Into ghi789
# Reword both
jj describe -r abc123 -m "feat: implement feature X with tests"
jj describe -r ghi789 -m "feat: implement feature Y with error handling"
# Verify
jj log -r 'main..@'
# @ ghi789 feat: implement feature Y with error handling
# ○ abc123 feat: implement feature X with tests
# Test each
jj new abc123 && nix build .# && jj new @-
jj new ghi789 && nix build .# && jj new @-
# Set bookmark
jj bookmark set feature-xy -r @
If any step fails, jj undo backs out immediately.
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.
Rebase winner onto main (preserves commit history):
# Rebase experiment commits onto main
jj rebase -s 'main..exp-1-winner' -d main
# Move main bookmark to include these commits
jj bookmark set main -r exp-1-winner
# Push
jj git push --bookmark main
Squash winner into main (clean single commit):
# Create new commit with all experiment changes
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: ..."
# Move main bookmark
jj bookmark set main -r @
# Push
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).
# Main experiment
jj bookmark create exp-1-parser -r main
# Sub-experiment 1a: regex approach
jj new exp-1-parser
jj describe -m "[exp-1a] feat: regex parser"
jj bookmark create exp-1a-regex -r @
# Sub-experiment 1b: PEG approach
jj new exp-1-parser # Branch from same point
jj describe -m "[exp-1b] feat: PEG parser"
jj bookmark create exp-1b-peg -r @
# Compare approaches
jj diff -r 'exp-1-parser..exp-1a-regex'
jj diff -r 'exp-1-parser..exp-1b-peg'
# Choose winner
jj rebase -s exp-1b-peg -d main
jj bookmark set exp-1-parser -r exp-1b-peg
jj bookmark delete exp-1a-regex
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."
# Merge to main
jj bookmark set main -r @
jj git push --bookmark main
# Continue improving in workspace
jj workspace add ../myproject-exp-4-cache -r @
# Iterate on implementation
# When stable, remove flag in follow-up commits
Effective jj session:
# Starting work
jj git fetch
jj log
jj new <base> # Or work on existing @
# During work
jj describe -m "message" # When purpose clear
jj split # When changes diverge
jj squash # When changes belong together
jj undo # On mistakes
# Preparing to push
jj log -r 'main..@'
jj describe -r <commits> # Clean up descriptions
# Squash/split as needed
jj bookmark set <name> -r @
jj git push --bookmark <name>
# After mistakes
jj op log
jj op restore <id>
Jujutsu operates in colocated mode with existing git repositories, allowing seamless interoperation.
# In existing git repository
cd /path/to/git/repo
jj git init --colocate
# Verify both .git/ and .jj/ exist
ls -la
# All git branches become jj bookmarks
jj log
Alternative initialization:
# Clone git repo with jj (automatically colocated)
jj git clone <url> <directory>
# Initialize new jj repo backed by git
jj git init --git-repo=.
In colocated mode:
jj logjj git import imports git changes (automatic in colocated mode)jj git export exports to git refs (automatic in colocated mode)Workflow:
jj log to see imported changesNote 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:
# Attach HEAD to branch (only needed when using git commands directly)
git checkout main
git switch main # Modern git (v2.23+)
jj operations may detach HEAD again. This is expected behavior.
# Fetch from remote
jj git fetch --all-remotes
# Track remote bookmark
jj bookmark track <name>@<remote>
# Push bookmark
jj git push --bookmark <name>
# Push all changed bookmarks
jj git push --all
# Push all tracked bookmarks
jj git push --tracked
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)# After push, log shows only unpushed commits
jj git push --bookmark main
jj log # Shows @ and maybe a few commits, then ~
# The ◆ symbol marks immutable commits (on remotes)
# The ~ symbol indicates hidden history below
# See full history including pushed commits
jj log -r 'all()'
jj log -r '::@' # All ancestors of working copy
jj log -r 'main@origin::' # Everything since remote main
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.
Since colocated mode maintains both .git/ and .jj/, you can revert to git-only operations at any time:
# Option 1: Simply stop using jj commands
# Git continues working normally with the .git/ directory
# All bookmarks exist as git branches
git status
git log
git checkout <branch>
# Option 2: Remove jj entirely (preserves all history in git)
rm -rf .jj/
# Repository is now pure git
# All jj bookmarks remain as git branches
# All commits remain in git history
# Operation log is lost, but git reflog provides similar functionality
# Option 3: Continue using both tools
# jj and git commands can be mixed freely
# Use jj for advanced history editing
# Use git when needed for specific operations
Key insight: Colocated mode provides a safe, reversible migration path. Your git repository is never at risk - jj adds capabilities without replacing git.
Query language for selecting commits:
# Symbols
@ # Current working-copy commit
<workspace>@ # Working-copy commit in named workspace
<bookmark> # Commit pointed to by bookmark
<bookmark>@<remote> # Remote bookmark position
root() # Root commit (all zeros)
# Operators
A..B # Range: reachable from B but not A
A::B # Path: all commits between A and B
A | B # Union: commits in either
A & B # Intersection: commits in both
~A # Negation: commits not in A
A ~ B # Difference: commits in A but not B
# Functions
mine() # Your commits (by configured email)
author("pattern") # Commits by author
description(glob:"pattern") # Commits with matching description
file("path") # Commits modifying path
~/path # Commits modifying path (shorthand)
empty() # Empty commits (no changes)
conflict() # Commits with conflicts
merge() # Merge commits
bookmarks() # All bookmarked commits
working_copies() # All working-copy commits
# Complex queries
main..@ # Commits ahead of main
mine() & ~bookmarks() # Your unbookmarked commits
description(glob:"WIP*") & main..@ # WIP commits in current branch
conflict() & main..@ # Conflicts in current work
# Create and manage commits
jj new <base> # New commit on base
jj new <parent1> <parent2> # New development join (multi-parent)
jj describe -m "message" # Describe current commit
jj commit # Move @ changes to parent
jj split # Split @ interactively
jj edit <commit> # Make commit the @
# Move changes between commits
jj squash # Move @ into parent
jj squash -r <commit> # Squash commit into parent
jj squash --from <src> --into <dst> # Move changes between any commits
jj squash -i # Interactive squash (choose hunks)
jj absorb # Auto-distribute @ to ancestors
# Rewrite history
# In a multi-parent development join, NEVER target @ with rebase or describe:
# jj rebase -r @ / --revisions @ relocates the shared [wip] off the join (drains its working copy under
# concurrency, drags the pushed wip deploy bookmark, breaks the one-child invariant); jj describe @ consumes
# the wip into content. Route down instead via jj absorb or
# jj squash --from @ --into/--insert-after <target> --keep-emptied.
# See "Composite working copy maintenance" and ~/.claude/skills/jj-version-control/SKILL.md §"Development join".
jj rebase -r <commit> -d <dest> # Move commit to new parent
jj rebase -s <commit> -d <dest> # Move commit and descendants
jj rebase -r <commit> -A <after> # Insert after commit
jj rebase -r <commit> -B <before> # Insert before commit
jj abandon <commit> # Remove commit, rebase descendants
# Bookmarks
jj bookmark create <name> # Create at @
jj bookmark create <name> -r <commit> # Create at commit
jj bookmark set <name> -r <commit> # Move bookmark
jj bookmark rename <old> <new> # Rename bookmark
jj bookmark delete <name> # Delete bookmark
jj bookmark list # List all bookmarks
jj bookmark track <name>@<remote> # Track remote bookmark
# Workspaces
jj workspace add <path> -r <commit> # Create workspace
jj workspace list # List workspaces
jj workspace forget <name> # Stop tracking workspace
jj workspace update-stale # Update stale workspace
# History and recovery
jj log # View history
jj log -r <revset> # View specific commits
jj diff -r <commit> # Show changes in commit
jj show <commit> # Show commit details
jj evolog -r <commit> # Evolution log of change
jj op log # Operation log
jj op show <id> # Show operation details
jj undo # Undo last operation
jj op restore <id> # Restore to operation
# Git interop
jj git init --colocate # Initialize colocated repo
jj git clone <url> # Clone git repo with jj
jj git fetch # Fetch from remote
jj git push --bookmark <name> # Push bookmark
jj git push --all # Push all changed bookmarks
After working session, provide summary:
# Operation-focused (shows actual work including undos)
jj op log --limit 20
# Commit-focused (shows commits not yet on bookmarks)
jj log -r 'bookmarks()..@'
# Combined view for complete picture
jj op log --limit N # Where N covers session
jj log -r 'main..@' # Commits in current work
Use explicit operation IDs from session start for precise ranges.
-m flag with commands that can prompt (jj describe, jj describe -r, jj split <paths>); verify unfamiliar commands with jj [subcommand] --help before execution@ is not exported to git until frozen; execute jj new after jj describe -m "msg" to make commits visible in git and maintain synchronization--help first and check for -m, --message flag or interactive keywords@ 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")