Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions .claude/skills/clean-commits/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
name: clean-commits
description: Reorganize messy git commit history on a feature branch into clean, logical, reviewer-friendly commits. Use this skill when the user types /clean-commits, or asks to "clean up commits", "reorganize commits", "rewrite history", "squash commits for review", "make commits easier to review", or anything about restructuring git history on their current branch. Also trigger when the user mentions their PR has too many commits, messy history, or WIP commits they want to clean up before review.
---

# Clean Commits

Rewrites the commit history on the current feature branch into clean, logical commits optimized for code review. This is a history rewrite only — the final tree state is identical to the original.

## Workflow

### Phase 1: Analyze

1. **Detect the base.** Find where this branch diverges from its upstream:
```bash
# Check if the branch tracks a remote with a PR target
gh pr view --json baseRefName 2>/dev/null
# Fall back to merge-base with main/master
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master
```
Only care about commits between the base and HEAD. Ignore commits that are already on the base branch.

2. **Read the history.** For each commit on this branch:
```bash
git log <base>..HEAD --reverse --format="%h %s" --stat
Comment on lines +16 to +25
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Detect the base” section runs gh pr view --json baseRefName but doesn’t show how to extract the branch name (or compute the merge-base from it). As written it just prints JSON and isn’t actionable for determining <base>. Consider using --jq to extract .baseRefName and then running git merge-base HEAD <baseRefName> (or document the exact next step).

Suggested change
# Check if the branch tracks a remote with a PR target
gh pr view --json baseRefName 2>/dev/null
# Fall back to merge-base with main/master
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master
```
Only care about commits between the base and HEAD. Ignore commits that are already on the base branch.
2. **Read the history.** For each commit on this branch:
```bash
git log <base>..HEAD --reverse --format="%h %s" --stat
# Try to detect the PR's base branch (e.g., main) and compute the merge-base
BASE_BRANCH="$(gh pr view --json baseRefName --jq '.baseRefName' 2>/dev/null || echo '')"
if [ -n "$BASE_BRANCH" ]; then
BASE_COMMIT="$(git merge-base HEAD "$BASE_BRANCH")"
else
# Fall back to merge-base with main/master
BASE_COMMIT="$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master)"
fi
echo "Using base commit: $BASE_COMMIT"

Only care about commits between the base and HEAD. Ignore commits that are already on the base branch.

  1. Read the history. For each commit on this branch:
    git log "$BASE_COMMIT"..HEAD --reverse --format="%h %s" --stat

Copilot uses AI. Check for mistakes.
```
Understand what each commit does, which files it touches, and how changes relate to each other.

3. **Identify the commit message convention.** Look at existing commit messages for prefixes like `[DX-793]`, `feat:`, `fix:`, ticket numbers, etc. The rewritten commits must follow the same convention.

### Phase 2: Plan

Group changes into logical commits. The goal is to make each commit independently reviewable with a clear purpose. Common groupings (adapt to what's actually in the branch):

| Priority | Category | Why separate |
|----------|----------|-------------|
| 1 | Infrastructure / utilities | Small, foundational — reviewer needs to understand these first |
| 2 | Base class / framework changes | The API that other code depends on |
| 3 | Mechanical migrations | Same pattern across many files — reviewer can skim. Note file count in the plan |
| 4 | Behavioral / logic changes | The interesting stuff — small diffs that deserve careful review |
| 5 | Test infrastructure + test updates | Tests grouped together, after the code they test |
| 6 | E2e / integration test fixes | Different environment, different review |
| 7 | Documentation | Docs last so reviewer sees the spec after the code |
Comment on lines +35 to +43
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table under “Common groupings” uses || at the start of each row, which renders as an extra empty column (and may not render as a table in some Markdown parsers). Use standard Markdown table syntax with single leading pipes (or no leading pipe) so it renders consistently.

Copilot uses AI. Check for mistakes.

**Key principles for grouping:**
- Separate mechanical changes (same pattern in 50+ files) from behavioral changes (logic that matters). This is the highest-value split — reviewers can skim the mechanical commit and focus on the behavioral one.
- If a file appears in multiple logical groups (e.g., got a mechanical migration AND a behavioral fix), use intermediate states from the original commits so each new commit shows only its logical change.
- Keep test changes separate from source changes where practical.
- Keep docs separate.
- Aim for 5-10 commits. Fewer than 5 usually means a commit is doing too much. More than 10 usually means over-splitting.

**Present the plan to the user as a table:**
```
| # | Commit message | Files | Purpose |
|---|---------------|-------|---------|
| 1 | [PREFIX] Add X utility | 1 | Core utility function |
| 2 | [PREFIX] Add Y to base command | 3 | Framework methods |
| 3 | [PREFIX] Migrate commands to use X | ~80 | Mechanical — same pattern everywhere |
| ...
```

Comment on lines +52 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language specifier to the fenced code block.

The code block showing the example table should specify a language (e.g., markdown or text) for proper rendering and to satisfy markdown linting rules.

📝 Proposed fix
 **Present the plan to the user as a table:**
-```
+```text
 | # | Commit message | Files | Purpose |

As per coding guidelines, static analysis tool markdownlint-cli2 reports: "Fenced code blocks should have a language specified (MD040)".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Present the plan to the user as a table:**
```
| # | Commit message | Files | Purpose |
|---|---------------|-------|---------|
| 1 | [PREFIX] Add X utility | 1 | Core utility function |
| 2 | [PREFIX] Add Y to base command | 3 | Framework methods |
| 3 | [PREFIX] Migrate commands to use X | ~80 | Mechanical — same pattern everywhere |
| ...
```
**Present the plan to the user as a table:**
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/clean-commits/SKILL.md around lines 52 - 61, The fenced code
block under the "**Present the plan to the user as a table:**" section is
missing a language tag causing MD040; update the opening fence for the sample
table to include a language specifier (for example change "```" to "```text" or
"```markdown") so the example table is properly recognized by markdownlint and
rendered correctly.

**Wait for user approval before proceeding.** They may want to adjust groupings or messages.

### Phase 3: Execute

1. **Create a backup branch:**
```bash
BRANCH=$(git branch --show-current)
ORIGINAL_HEAD=$(git rev-parse HEAD)
git branch "${BRANCH}-backup" "$ORIGINAL_HEAD"
```

2. **Create a temporary rewrite branch from the base:**
```bash
BASE_COMMIT=<detected base>
git checkout -b "${BRANCH}-rewrite" "$BASE_COMMIT"
```

3. **Build each commit.** For each planned commit, checkout the right files and commit:

For files that only appear in one logical commit, checkout from the final HEAD:
```bash
git checkout "$ORIGINAL_HEAD" -- path/to/file.ts
```

For files that appear in multiple logical commits (e.g., mechanical migration in commit 3, behavioral fix in commit 5), use intermediate states from the original history. Find the right source commit:
- The "mechanical only" state: checkout from the original commit that did the migration
- The "with behavioral fix" state: checkout from the final HEAD

**Use a shell script file for complex checkouts** to avoid quoting issues with inline bash. Write the file list to a script, then execute it:
```bash
# Write checkout script to /tmp to avoid shell quoting issues
cat > /tmp/checkout-files.sh << 'SCRIPT'
#!/bin/bash
set -e
# Files needing intermediate state
INTERMEDIATE_FILES=(path/to/file1.ts path/to/file2.ts)
for f in "${INTERMEDIATE_FILES[@]}"; do
git checkout <intermediate-commit> -- "$f"
done
# Files needing final state
git diff --name-only <base> <final> -- src/some/path/ | \
grep -v 'special-file' | \
while IFS= read -r f; do git checkout <final> -- "$f"; done
SCRIPT
Comment on lines +102 to +105
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the checkout script example, placeholders switch between <base>/<final> and earlier variables like BASE_COMMIT / ORIGINAL_HEAD. This inconsistency makes the steps harder to follow and easier to copy/paste incorrectly. Consider using the same placeholder names throughout (or reference the variables already introduced).

Copilot uses AI. Check for mistakes.
bash /tmp/checkout-files.sh
```

Stage and commit (NO Co-Authored-By):
```bash
git add -A
git commit -m "[PREFIX] Commit message here"
```

4. **Swap the branch pointer:**
```bash
git branch -f "$BRANCH" "${BRANCH}-rewrite"
git checkout "$BRANCH"
git branch -d "${BRANCH}-rewrite"
```

### Phase 4: Verify

1. **Diff check — this is non-negotiable.** The final tree MUST match the original:
```bash
git diff "$BRANCH" "${BRANCH}-backup" -- <relevant paths>
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 4 says the diff check is “non-negotiable” and that the final tree MUST match, but the suggested command scopes the diff to <relevant paths>, which could miss discrepancies elsewhere. To guarantee identical tree state, run an unscoped diff (or explicitly state that it must be run without a pathspec).

Suggested change
git diff "$BRANCH" "${BRANCH}-backup" -- <relevant paths>
git diff "$BRANCH" "${BRANCH}-backup"

Copilot uses AI. Check for mistakes.
```
If this produces ANY output, something went wrong. Do not proceed — investigate and fix.

2. **Build and test:**
```bash
pnpm prepare # or the project's build command
pnpm exec eslint . # or the project's lint command
pnpm test:unit # or the project's test command
```

3. **Show the final history to the user:**
```bash
git log <base>..HEAD --oneline --stat
```

### Phase 5: Push

Ask the user if they want to force push. Only after explicit approval:
```bash
git push --force-with-lease origin "$BRANCH"
```

Remind the user the backup branch is available locally:
```
git branch -d ${BRANCH}-backup # when you're happy with the result
```
Comment on lines +142 to +152
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language specifier to the fenced code block.

The code block showing the backup branch cleanup command should specify bash as the language for proper rendering and to satisfy markdown linting rules.

📝 Proposed fix
 Remind the user the backup branch is available locally:
-```
+```bash
 git branch -d ${BRANCH}-backup  # when you're happy with the result
</details>

As per coding guidelines, static analysis tool markdownlint-cli2 reports: "Fenced code blocks should have a language specified (MD040)".

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>

[warning] 150-150: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @.claude/skills/clean-commits/SKILL.md around lines 142 - 152, Update the
fenced code block that shows the backup branch cleanup in "Phase 5: Push" so it
includes the language specifier "bash"; locate the block containing the literal
line "git branch -d ${BRANCH}-backup # when you're happy with the result" and
change the opening fence to specify bash (e.g., ```bash) to satisfy MD040 and
markdownlint-cli2.


</details>

<!-- fingerprinting:phantom:triton:puma -->

<!-- This is an auto-generated comment by CodeRabbit -->


## Rules

- **NEVER** include `Co-Authored-By: Claude` or any co-author trailer in commit messages
- **NEVER** change the code — this is a history rewrite only. The final tree state must be identical to the original
- **ALWAYS** create a backup branch before any destructive operation
- **ALWAYS** get user approval on the commit plan before executing
- **ALWAYS** verify with `git diff` that the final state matches the original before declaring success
- Follow the existing commit message convention on the branch
- Use `--force-with-lease` (not `--force`) when pushing to catch unexpected remote changes
- When a commit touches 50+ files with the same pattern, call it out as "mechanical" in the plan so reviewers know they can skim it
Loading