/clean-history Command
Inspired by git-based "reimplement branch with clean history" workflow
Key difference: jj doesn't require reimplementing - just reorganize existing changes
description: Reorganize the current branch into a clean, narrative-quality commit history argument-hint: [bookmark-name] allowed-tools: Bash(jj:), Bash(jj-hunk:), Bash(gh:*) model: opus
Context
- Current change: !
jj log -r @ --no-graph - Bookmarks: !
jj bookmark list - Commits since main: !
jj log -r 'trunk()..@' --no-graph - Full diff against main: !
jj diff -r 'trunk()' --stat
Task
Reorganize changes into a clean, narrative-quality commit history suitable for reviewer comprehension.
Bookmark Name: Use $ARGUMENTS if provided.
Steps
-
Validate the working copy
- Ensure no conflicts:
jj status - Review the full scope of changes to understand what needs organizing
- Ensure no conflicts:
-
Combine all changes into one commit
# Squash all commits since trunk into the current commit jj squash --from 'all:trunk()..@-' --into @ -
Edit the combined commit
jj edit @ -
Plan the commit storyline
- Study the changes:
jj-hunk list | jq 'keys' - Group files by logical concern (e.g., schema, migrations, services, tests)
- Order commits as a narrative: setup → core logic → integration → polish
- Study the changes:
-
Split iteratively using jj-hunk
First, inspect the hunks to understand what you're working with:
jj-hunk listExample output:
{ "src/db/schema.ts": [ {"index": 0, "type": "insert", "added": "import { pgTable }...\n"}, {"index": 1, "type": "insert", "added": "export const users = pgTable...\n"}, {"index": 2, "type": "insert", "added": "export const posts = pgTable...\n"} ], "src/api/routes.ts": [ {"index": 0, "type": "replace", "removed": "// TODO\n", "added": "app.get('/users', ...);\n"}, {"index": 1, "type": "insert", "added": "app.get('/posts', ...);\n"} ], "src/lib/utils.ts": [ {"index": 0, "type": "replace", "removed": "function old()...\n", "added": "function new()...\n"}, {"index": 1, "type": "insert", "added": "export function helper()...\n"}, {"index": 2, "type": "delete", "removed": "// dead code\n"} ] }File-level selection — when all hunks in a file belong together:
# Keep entire file, reset everything else jj-hunk split '{"files": {"src/db/schema.ts": {"action": "keep"}}, "default": "reset"}' "feat: add database schema"Hunk-level selection — when a file has mixed concerns:
# src/lib/utils.ts has refactoring (hunks 0, 2) and new feature (hunk 1) # Extract just the refactoring hunks jj-hunk split '{"files": {"src/lib/utils.ts": {"hunks": [0, 2]}}, "default": "reset"}' "refactor: clean up utils" # Now hunk 1 remains in working copy for the feature commitMixed selection — combine file-level and hunk-level:
# Keep all of schema.ts, but only hunk 0 from routes.ts jj-hunk split '{"files": {"src/db/schema.ts": {"action": "keep"}, "src/api/routes.ts": {"hunks": [0]}}, "default": "reset"}' "feat: add users endpoint" # Next commit: remaining routes.ts hunk 1 jj-hunk split '{"files": {"src/api/routes.ts": {"hunks": [1]}}, "default": "reset"}' "feat: add posts endpoint"Typical narrative sequence:
# 1. Infrastructure/setup first jj-hunk split '{"files": {"src/db/schema.ts": {"action": "keep"}, "drizzle.config.ts": {"action": "keep"}}, "default": "reset"}' "feat: add database schema" # 2. Core logic jj-hunk split '{"files": {"src/lib/utils.ts": {"hunks": [0, 2]}}, "default": "reset"}' "refactor: prepare utils for new feature" # 3. Feature implementation jj-hunk split '{"files": {"src/lib/utils.ts": {"action": "keep"}, "src/api/routes.ts": {"hunks": [0]}}, "default": "reset"}' "feat: add user routes" # 4. Remaining changes described as final commit jj describe -m "feat: add post routes" -
Describe the final commit
jj describe -m "feat: final piece of the implementation" -
Verify the result
# Check the commit structure jj log -r 'trunk()..@' # Verify each commit has sensible content jj diff -r <rev> --stat # Confirm total diff matches original intent jj diff -r 'trunk()' --stat -
Set bookmark and push
jj bookmark set <name> -r @ jj git push --bookmark <name> -
Create pull request
gh pr create --title "feat: description" --body "$(cat <<'EOF' ## Summary - bullet points ## Test plan - [ ] verification steps EOF )"
Spec Reference
| Spec | Effect |
|------|--------|
| {"action": "keep"} | Include all changes in file |
| {"action": "reset"} | Exclude file from this commit |
| {"hunks": [0, 2]} | Include only hunks 0 and 2 |
| "default": "reset" | Unlisted files excluded (safer) |
| "default": "keep" | Unlisted files included |
Rules
- Never add yourself as an author or contributor
- Never include "Generated with Claude Code" or "Co-Authored-By" lines
- Each commit should introduce a single coherent idea
- Commit messages should read like a tutorial progression
- The final diff against trunk must match the original intent