Dotfiles jj
Jujutsu (jj) version control workflow, commands, and best practices. Use when working with version control in jj-enabled repos. Covers commits, bookmarks, workspaces, and safe push patterns.
git clone https://github.com/megalithic/dotfiles
T=$(mktemp -d) && git clone --depth=1 https://github.com/megalithic/dotfiles "$T" && mkdir -p ~/.claude/skills && cp -r "$T/docs/skills/jj" ~/.claude/skills/megalithic-dotfiles-jj && rm -rf "$T"
docs/skills/jj/SKILL.mdJujutsu (jj) Version Control
Overview
CRITICAL: Always use
jj commands instead of git for ALL version control operations in jj-enabled repos. Never use raw git commands directly.
Jujutsu is a Git-compatible VCS with automatic snapshots, mutable history, and conflict-free parallel work. This dotfiles repo is jj-initialized with full git coexistence.
Quick Reference Card
STATUS: jj status (jj s) DIFF: jj diff (jj d) LOG: jj log (jj l) NEW COMMIT: jj new -m "message" DESCRIBE: jj describe -m "msg" (jj dm "msg") SQUASH: jj squash -m "message" FETCH: jj git fetch (jj g fetch) PUSH: jj git push (jj push) REBASE: jj rebase -d main (jj rb -d main) BOOKMARK: jj bookmark set main -r @ (jj main)
Decision Trees
"I need to start new work"
Need to start new work? │ ├─▶ Standard workflow (RECOMMENDED): │ └─▶ jj new -m "feat: description" │ └─▶ Work in default workspace │ ├─▶ Need isolation? (⚠️ WIP - workspace scripts not stable) │ └─▶ For now: just use jj new in default workspace │ └─▶ Workspace scripts under development │ └─▶ Multiple parallel features? └─▶ jj new main -m "Feature A" └─▶ jj new main -m "Feature B" └─▶ Use jj edit <change-id> to switch
"I need to save/commit my work"
Need to save work? │ ├─▶ Just want to record progress (local)? │ └─▶ jj describe -m "work in progress" │ (or just keep working - jj auto-snapshots) │ ├─▶ Ready to finalize commit message? │ └─▶ jj describe -m "feat(scope): detailed message │ │ - What changed │ - Why it changed" │ ├─▶ Want to squash into parent? │ └─▶ jj squash -m "combined message" │ └─▶ Want to split into multiple commits? └─▶ jj split (interactive - opens editor)
"I need to push to remote"
Ready to push? │ ├─▶ Check what will be pushed: │ └─▶ jj log -r 'main@origin::main' │ ├─▶ Nothing to push (main == main@origin)? │ └─▶ First move main to current: jj bookmark set main -r @ │ ├─▶ Remote is ahead (commits on origin we don't have)? │ └─▶ jj git fetch │ └─▶ jj rebase -d main@origin │ └─▶ Then try push again │ └─▶ Ready to push? └─▶ ASK USER FIRST - Never push without consent! └─▶ jj git push --bookmark main
"I have conflicts"
Conflicts detected? │ ├─▶ See conflict markers: │ └─▶ jj status (shows conflicted files) │ └─▶ Look for <<<<<<< markers in files │ ├─▶ Resolve conflicts: │ └─▶ Edit files to resolve │ └─▶ Remove conflict markers │ └─▶ jj status (verify resolved) │ ├─▶ Want to use a merge tool? │ └─▶ jj resolve <file> │ └─▶ Want to abort and try different approach? └─▶ jj op log (find pre-conflict state) └─▶ jj op restore <op-id>
"Something went wrong"
Need to recover? │ ├─▶ Undo last operation: │ └─▶ jj undo │ ├─▶ See operation history: │ └─▶ jj op log │ └─▶ jj op restore <op-id> │ ├─▶ Abandon current change: │ └─▶ jj abandon │ ├─▶ Discard uncommitted edits: │ └─▶ jj restore │ └─▶ Find lost commit: └─▶ jj evolog (shows change evolution) └─▶ jj op log (shows all operations)
Configuration
User's Custom Aliases
These aliases are configured and available:
| Alias | Expands To | Purpose |
|---|---|---|
| | Bookmark management |
| | Show diff |
| | Describe with message |
| | Describe (opens editor) |
| | Git subcommand |
| | Show log |
| | Log with full descriptions |
| | Move main to current |
| | Push to remote |
| | Rebase |
| | Status |
| (moves closest bookmark to parent) | Pull bookmark down |
Workflow Aliases (Multi-Command)
These aliases use
jj util exec to chain multiple commands:
| Alias | Usage | Purpose |
|---|---|---|
| | Fetch + rebase onto origin (default: main) |
| | Fetch + new commit from main@origin |
| | New commit from current (no fetch) |
| | New commit on PR branch + confirm push |
| | Squash into parent + confirm push |
Example workflows:
# Start a new feature jj up # Sync with origin/main first jj feat "feat: add user auth" # Create new commit from main@origin jj bookmark create user-auth # Create bookmark for PR # Work on existing PR jj up # Sync with origin # ... make changes ... jj pr-fix "fix: address review feedback" # New commit, asks to push # Quick fix on existing PR # ... make small changes ... jj fixup # Squash into parent, asks to push
User's Custom Revsets
| Revset | Meaning |
|---|---|
| |
| Work between trunk and @, used as default log |
| Ancestors of reachable mutable commits |
| Stack at specific revision |
| Stack with depth limit |
| Find nearest bookmark ancestor |
UI Settings
- Pager: delta (with syntax highlighting)
- Graph style: curved
- Editor: neovim
- Signing: SSH with 1Password integration
- Default command:
(runs when just typingjj log
)jj
Revset Syntax Reference
Revsets are jj's query language for selecting commits.
Basic Selectors
| Revset | Meaning |
|---|---|
| Current working copy commit |
| Parent of @ |
| Grandparent of @ |
| Repository root commit |
| All head commits |
| Bookmark named "main" |
| Remote tracking bookmark |
| Commit/change ID (prefix match) |
Operators
| Operator | Meaning | Example |
|---|---|---|
| Ancestors of x (inclusive) | |
| Descendants of x (inclusive) | |
| Range from x to y | |
| Parent of x | |
| Children of x | |
| Union (x or y) | |
| Intersection (x and y) | |
| Difference (x but not y) | |
| Negation (not x) | |
Functions
| Function | Returns |
|---|---|
| All commits |
| Empty set |
| Commits in x with no descendants in x |
| Commits in x with no ancestors in x |
| All ancestors of x |
| All descendants of x |
| Commits reachable from x through y |
| Ancestors and descendants connecting x |
| Direct parents of x |
| Direct children of x |
| Non-immutable commits |
| Immutable commits (usually pushed) |
| Commits with bookmarks |
| Commits matching bookmark pattern |
| Commits with remote bookmarks |
| Commits with tags |
| Git HEAD |
| Empty commits |
| Commits with conflicts |
| Commits by author |
| Commits with matching description |
| Commits touching file |
Common Revset Patterns
# What will I push? jj log -r 'main@origin::main' # My recent work jj log -r '@::' # Unpushed work on any bookmark jj log -r 'bookmarks() ~ remote_bookmarks()' # Find commits with "fix" in message jj log -r 'description("fix")' # Commits I authored jj log -r 'author("seth")' # Commits touching specific file jj log -r 'file("home/programs/ai")' # All workspace bookmarks jj log -r 'bookmarks(glob:"ws/*")'
Template Language
Templates control output formatting with
-T flag.
Common Template Variables
| Variable | Type | Description |
|---|---|---|
| CommitId | Full commit hash |
| ChangeId | jj change ID |
| String | Commit message |
| Signature | Author info |
| Signature | Committer info |
| String | Working copy info |
| List | Bookmarks pointing here |
| List | Tags pointing here |
| Bool | Is this git HEAD? |
| Bool | Is commit empty? |
| Bool | Has conflicts? |
| Bool | Is root commit? |
Template Methods
commit_id.short() # Short hash commit_id.short(8) # 8-char hash change_id.shortest() # Shortest unique prefix description.first_line() # First line only author.name() # Author name author.email() # Author email author.timestamp() # Commit time
Template Syntax
# Simple template jj log -T 'change_id.short() ++ " " ++ description.first_line() ++ "\n"' # Conditional jj log -T 'if(empty, "(empty) ") ++ description.first_line()' # With labels (for colors) jj log -T 'label("commit_id", commit_id.short())' # Check boolean jj log -r @ --no-graph -T 'if(empty, "true", "false")'
Workspace Scripts (⚠️ WORK IN PROGRESS)
WARNING: These workspace scripts are under active development and not fully functional yet. Do NOT rely on them for production work. Use standard jj commands (
,jj new, etc.) in the default workspace until these are stable.jj describe
Four custom scripts for AI agent workspace management:
jj-ws-claim
Claim or create a workspace for isolated work.
# Basic usage jj-ws-claim <task-id> # With options jj-ws-claim <task-id> --base <revision> --json # Example jj-ws-claim hs-memory-leaks # Creates: .workspaces/hs-memory-leaks/ # Creates bead task if doesn't exist
What it does:
- Creates
directory.workspaces/<task-id>/ - Creates jj workspace pointing to main repo
- Creates bead task for tracking (if
available)bd - Uses mkdir-based locking for concurrent safety
Exit codes:
- 0: Success
- 1: Invalid arguments
- 2: Not in jj repository
- 3: Workspace creation failed
- 4: Already in non-default workspace
jj-ws-complete
Complete work in a workspace and clean up.
# Complete current workspace jj-ws-complete # Complete specific workspace jj-ws-complete <workspace-name> # Options jj-ws-complete -r # Rebase if parallel branch jj-ws-complete --no-merge # Don't merge to main jj-ws-complete --no-cleanup # Keep workspace directory jj-ws-complete --reason "text" # Close reason for bead
What it does:
- Creates
bookmark for trackingws/<workspace-name> - Merges work to main (moves main bookmark)
- Closes associated bead task
- Cleans up workspace directory
Exit codes:
- 0: Success
- 1: Invalid arguments
- 2: Not in jj repository
- 3: In default workspace (nothing to complete)
- 4: Workspace not found
jj-ws-push
Review and push completed workspace work.
# List all completed workspace work jj-ws-push --list # Push specific bookmark jj-ws-push ws/<name> # Push all workspace bookmarks jj-ws-push --all # Force (skip confirmation) jj-ws-push -f ws/<name>
What it does:
- Lists ws/* bookmarks (completed workspace work)
- Shows diff/log for review
- Requires explicit confirmation before push
- Moves main and pushes to origin
jj-ws-status
Get current workspace status (useful for agents).
# Human readable jj-ws-status # Machine readable jj-ws-status --json
Returns:
- Workspace name and path
- Commit info (empty, conflicts, description)
- Associated bead task
- All available workspaces
Workspace Lifecycle (⚠️ WIP)
⚠️ NOT YET STABLE - Use standard jj workflow for now ┌─────────────────┐ │ jj-ws-claim │ Create isolated workspace └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Work in │ Make changes, jj describe │ workspace │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ jj-ws-complete │ Creates ws/* bookmark, merges to main └────────┬────────┘ │ ▼ ┌─────────────────┐ │ jj-ws-push │ User reviews and pushes (requires consent!) └─────────────────┘
Current Recommended Workflow (Stable):
jj new -m "description" → work → jj describe → jj bookmark set main -r @ → ask user to push
Command Reference
Core Commands
| Command | Purpose | Example |
|---|---|---|
| Show working copy changes | |
| Show current change diff | |
| Show revision history | |
| Show commit details | |
| Create new change | |
| Update commit message | |
| Switch to editing a change | |
| Discard a change | |
History Manipulation
| Command | Purpose | Example |
|---|---|---|
| Merge current into parent | |
| Split change into multiple | |
| Move change to new parent | |
| Auto-distribute fixes | |
| Copy changes | |
| Make revisions siblings | |
Bookmarks (like git branches)
| Command | Purpose | Example |
|---|---|---|
| List bookmarks | |
| Move bookmark | |
| Create bookmark | |
| Delete bookmark | |
| Move bookmark | (alias) |
Git Integration
| Command | Purpose | Example |
|---|---|---|
| Fetch from remote | |
| Push to remote | |
| Clone git repo | |
| Init in git repo | |
| Export to .git | |
| Import from .git | |
Recovery
| Command | Purpose | Example |
|---|---|---|
| Undo last operation | |
| Redo undone operation | |
| Show operation history | |
| Restore to past state | |
| Show change evolution | |
| Restore file content | |
Workspaces
| Command | Purpose | Example |
|---|---|---|
| List workspaces | |
| Create workspace | See scripts above |
| Remove workspace | |
| Show workspace root | |
| Update stale ws | Auto with config |
File Operations
| Command | Purpose | Example |
|---|---|---|
| List tracked files | |
| Show file at rev | |
| Change permissions | |
| Sparse checkout | |
Common Workflows
Daily Work Pattern
# 1. Start day - fetch latest jj git fetch # 2. Check if behind jj log -r 'main@origin::main' # 3. Rebase if needed jj rebase -d main@origin # 4. Start new work jj new -m "feat: today's work" # 5. Work... (changes auto-tracked) # 6. Describe when ready jj describe -m "feat(scope): what I did Detailed description here." # 7. Move main bookmark jj bookmark set main -r @ # 8. Push (with user consent) jj git push
Parallel Features
# Create feature branches off main jj new main -m "Feature A" # Now at feature-a change jj new main -m "Feature B" # Creates new change off main # Switch between them jj edit <change-id-a> # Work on A jj edit <change-id-b> # Work on B # Merge both to main when done jj rebase -r <change-id-a> -d main jj bookmark set main -r <change-id-a> jj rebase -r <change-id-b> -d main jj bookmark set main -r <change-id-b>
Sync with Remote (Before Push)
# Fetch latest jj git fetch # Check divergence jj log -r 'main@origin..main' # Local-only commits jj log -r 'main..main@origin' # Remote-only commits # If remote is ahead, rebase jj rebase -d main@origin # Then push jj git push --bookmark main
Clean Up History
# Squash multiple small commits into one jj squash --from <start> --into <target> # Split a big commit jj split # Interactive, choose files # Reword any commit jj describe -r <rev> -m "new message"
Safety Rules
NEVER Do
- NEVER push without explicit user consent
- NEVER use git commands directly (use jj equivalents)
- NEVER assume push will succeed (always fetch first)
- NEVER force push without extreme caution
ALWAYS Do
- ALWAYS start work with
- creates clean changejj new - ALWAYS check
before pushingjj log -r 'main@origin::main' - ALWAYS ask user before any push operation
- ALWAYS use headless/non-interactive flags (see below)
Interactive vs Headless Commands (CRITICAL)
CRITICAL: Many jj commands open an editor by default. AI agents MUST use headless flags to avoid hanging.
Commands That Open Editor (AVOID)
| Command | Opens Editor | Headless Alternative |
|---|---|---|
| YES - opens $EDITOR | |
| YES - opens $EDITOR | |
| YES - interactive | NO HEADLESS - ask user |
| YES - opens $EDITOR | |
| YES - opens merge tool | NO HEADLESS - ask user |
Always Use These Patterns
# WRONG - will hang waiting for editor jj describe jj squash jj commit # RIGHT - provide message inline jj describe -m "feat: add feature X" jj squash -m "combine: cleanup commits" jj commit -m "feat: complete feature"
Commands That Are Safe (Non-Interactive)
These commands never open an editor:
jj status # Safe jj diff # Safe jj log # Safe jj new -m "msg" # Safe (with -m) jj abandon # Safe jj git fetch # Safe jj git push # Safe jj bookmark set # Safe jj rebase # Safe jj edit # Safe jj undo # Safe jj op log # Safe jj op restore # Safe
When Interactive is Required
Some commands have no headless mode. For these, ask the user:
# jj split - no headless mode # Ask user: "I need to split this commit. Can you run `jj split` interactively?" # jj resolve - needs merge tool # Ask user: "There are conflicts. Can you resolve them with `jj resolve <file>`?"
Editor Timeout Prevention
If you accidentally run an interactive command:
- The command will hang waiting for editor
- Use Ctrl+C to abort
- Re-run with
flag-m
Transparency Protocol
When using jj in sessions, provide:
Inline Explanations
For each jj command, explain:
- What the command does
- Why you're running it
- Expected outcome
Running `jj git fetch` - This pulls the latest commits from origin without modifying the working copy. Needed to check if main is ahead before attempting to push.
End-of-Session Summary
## jj Commands Used This Session | Command | Purpose | |---------|---------| | `jj new -m "feat: ..."` | Started new unit of work | | `jj git fetch` | Pulled latest from remote | | `jj describe -m "..."` | Updated commit message | | `jj bookmark set main -r @` | Moved main to current | | `jj git push --bookmark main` | Pushed to origin (with consent) |
Beads Integration
ID Correlation Patterns
Beads tasks and jj work should be correlated through consistent naming:
| Bead Task ID | jj Bookmark | jj Workspace | Commit Reference |
|---|---|---|---|
| | | |
| | | |
Finding Correlated Work
# From bead task → find jj work bd show .dotfiles-abc # Get task details jj log -r 'description(".dotfiles-abc")' # Find commits referencing it jj bookmark list | grep -i abc # Find related bookmarks # From jj bookmark → find bead task jj log -r 'ws/abc' --no-graph -T 'description' # Get commit description bd show .dotfiles-abc # Look up task by ID extracted from commit # From workspace → find both jj-ws-status --json # Shows associated task ID bd show $(jj-ws-status --json | jq -r '.task.id') # Get task details
Workspace Scripts Auto-Correlation (⚠️ WIP)
Note: Workspace scripts are under development. For now, manually correlate tasks with commits using the patterns below.
The
jj-ws-* scripts (when stable) will automatically correlate:
# jj-ws-claim creates both (WIP) jj-ws-claim fix-lsp # Creates: .workspaces/fix-lsp/ # Creates: .dotfiles-fix-lsp (bead task) # jj-ws-complete references task in close (WIP) jj-ws-complete # Closes bead task with: "Completed in workspace fix-lsp [bookmark: ws/fix-lsp]" # jj-ws-status shows correlation (WIP) jj-ws-status --json | jq '.task' # {"id": ".dotfiles-fix-lsp", "status": "in_progress", "title": "..."}
Current Manual Workflow:
# 1. Create bead task bd create fix-lsp -t task -p P2 # 2. Start jj work with reference jj new -m "fix(lsp): address issue Task: .dotfiles-fix-lsp" # 3. Complete and close manually jj describe -m "fix(lsp): resolved issue Closes: .dotfiles-fix-lsp" bd close .dotfiles-fix-lsp
Standard Commit References
Always include task reference in commit messages:
# Short reference (in commit subject) jj describe -m "feat(lsp): add diagnostic filtering (.dotfiles-abc)" # Full reference (in commit body) jj describe -m "feat(lsp): add diagnostic filtering Implemented severity-based filtering for LSP diagnostics. Added configuration for per-language rules. Task: .dotfiles-abc Closes: .dotfiles-abc"
Finding Orphaned Work
# Commits without task references jj log -r 'all() ~ description("dotfiles-")' # Workspace bookmarks without closed tasks for bm in $(jj bookmark list | grep '^ws/' | awk '{print $1}'); do task_id=".dotfiles-${bm#ws/}" if bd show "$task_id" 2>/dev/null | grep -q "open"; then echo "Open task for $bm: $task_id" fi done # Bead tasks without jj work bd list --status=open | while read task; do if ! jj log -r "description(\"$task\")" --no-graph 2>/dev/null | head -1 | grep -q .; then echo "No commits for: $task" fi done
Link in Both Directions
Starting work (current stable workflow):
# 1. Create bead task bd create fix-lsp -t task -p P2 -d "Fix LSP diagnostic flooding" # Creates: .dotfiles-fix-lsp # 2. Start jj work with task reference jj new -m "fix(lsp): address diagnostic flooding Task: .dotfiles-fix-lsp" # 3. Update task status bd update .dotfiles-fix-lsp --status in_progress
Completing work (current stable workflow):
# 1. Describe final commit jj describe -m "fix(lsp): implement diagnostic filtering Added severity-based filtering for LSP diagnostics. Closes: .dotfiles-fix-lsp" # 2. Move main bookmark jj bookmark set main -r @ # 3. Close bead task bd close .dotfiles-fix-lsp --reason "Implemented in $(jj log -r @ --no-graph -T 'change_id.short(8)')" # 4. ASK USER before pushing # "Ready to push. Run: jj git push --bookmark main"
Known Issues and Limitations
jj Limitations
- No interactive rebase - use
,jj squash
insteadjj split - No staging area - all changes auto-tracked (feature, not bug)
- Conflicts stored in tree - unlike git stash conflicts
- Immutable after push - can't rewrite pushed commits easily
Common Gotchas
| Issue | Cause | Solution |
|---|---|---|
| "change is immutable" | Commit was pushed | Create new commit instead |
| Bookmark disappeared | Moved unexpectedly | + |
| Working copy conflict | Auto-merge failed | Edit files, remove markers |
| "no description" warning | Empty commit message | Use |
| Workspace stale | Another workspace changed repo | |
GitHub Actions Interaction
The dotfiles repo has a GitHub Action that updates flake.lock on Sundays. Always:
jj git fetch # Get flake.lock updates jj rebase -d main@origin # Rebase onto updated main
Self-Discovery Patterns
Finding Commands
# List all commands jj help # Help for specific command jj help <command> jj describe --help # Search command help jj help | grep -i <keyword>
Exploring Configuration
# List all config jj config list # Show specific config jj config get <key> # Config file location jj config path --user jj config path --repo
Inspecting Repository State
# All changes jj log -r 'all()' # All bookmarks jj bookmark list # Operation history jj op log # Change evolution jj evolog # Workspace state jj-ws-status --json
Revset Debugging
# Test revset (dry run) jj log -r '<revset>' --no-graph # Count matches jj log -r '<revset>' --no-graph | wc -l # Show IDs only jj log -r '<revset>' --no-graph -T 'change_id.short()'
Official Resources
- jj book: https://martinvonz.github.io/jj/latest/
- GitHub: https://github.com/martinvonz/jj
- Discord: https://discord.gg/dkmfj3aGQN
Directory Structure
~/.dotfiles/ # Main workspace (default) ├── .jj/ # jj data directory │ ├── repo/ # Repository data │ │ ├── store/ # Object store │ │ └── op_store/ # Operation store │ ├── working_copy/ # Working copy state │ └── workspace-ops.lock/ # Concurrent op lock (created by scripts) │ ├── .workspaces/ # Secondary workspaces (gitignored) │ └── <workspace-name>/ # Created by jj-ws-claim │ ├── .jj/ # Points to main repo │ └── (files) # Working copy │ ├── bin/ # Custom scripts │ ├── jj-ws-claim # Create workspace │ ├── jj-ws-complete # Complete workspace │ ├── jj-ws-push # Push workspace work │ └── jj-ws-status # Get workspace status │ └── _docs/ # Research/documentation ├── jj-workspace-conventions.md # Naming conventions └── jj-workspaces-research.md # Implementation research
Quick Troubleshooting
"Change is immutable"
# Can't modify pushed commits # Solution: create new change instead jj new -m "fix: corrected version"
"Bookmark moved unexpectedly"
jj op log # Find when it moved jj op restore <op-id> # Restore previous state
"Working copy is stale"
# Usually auto-resolves with auto-update-stale=true jj workspace update-stale
"Conflicts in working copy"
jj status # See conflicted files # Edit files to resolve <<<<<<< markers jj status # Verify resolved
"Can't push - remote ahead"
jj git fetch jj log -r 'main..main@origin' # See what's new jj rebase -d main@origin # Rebase onto remote jj git push # Now push
Lost Work
jj op log # Find operation before loss jj op restore <op-id> # Restore # Or find via evolution log jj evolog # Shows all versions of current change